outline_panel.rs

   1mod outline_panel_settings;
   2
   3use std::{
   4    cmp,
   5    ops::Range,
   6    path::{Path, PathBuf},
   7    sync::{atomic::AtomicBool, Arc},
   8    time::Duration,
   9};
  10
  11use anyhow::Context;
  12use collections::{hash_map, BTreeSet, HashMap, HashSet};
  13use db::kvp::KEY_VALUE_STORE;
  14use editor::{
  15    display_map::ToDisplayPoint,
  16    items::{entry_git_aware_label_color, entry_label_color},
  17    scroll::ScrollAnchor,
  18    DisplayPoint, Editor, EditorEvent, ExcerptId, ExcerptRange,
  19};
  20use file_icons::FileIcons;
  21use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
  22use gpui::{
  23    actions, anchored, deferred, div, px, uniform_list, Action, AnyElement, AppContext,
  24    AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId, EntityId,
  25    EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyContext, Model,
  26    MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, SharedString, Stateful,
  27    Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext,
  28    WeakView, WindowContext,
  29};
  30use itertools::Itertools;
  31use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
  32use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrev};
  33
  34use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings};
  35use project::{File, Fs, Item, Project};
  36use serde::{Deserialize, Serialize};
  37use settings::{Settings, SettingsStore};
  38use util::{RangeExt, ResultExt, TryFutureExt};
  39use workspace::{
  40    dock::{DockPosition, Panel, PanelEvent},
  41    item::ItemHandle,
  42    ui::{
  43        h_flex, v_flex, ActiveTheme, Color, ContextMenu, FluentBuilder, HighlightedLabel, Icon,
  44        IconName, IconSize, Label, LabelCommon, ListItem, Selectable, Spacing, StyledExt,
  45        StyledTypography,
  46    },
  47    OpenInTerminal, Workspace,
  48};
  49use worktree::{Entry, ProjectEntryId, WorktreeId};
  50
  51actions!(
  52    outline_panel,
  53    [
  54        ExpandSelectedEntry,
  55        CollapseSelectedEntry,
  56        ExpandAllEntries,
  57        CollapseAllEntries,
  58        CopyPath,
  59        CopyRelativePath,
  60        RevealInFileManager,
  61        Open,
  62        ToggleFocus,
  63        UnfoldDirectory,
  64        FoldDirectory,
  65        SelectParent,
  66    ]
  67);
  68
  69const OUTLINE_PANEL_KEY: &str = "OutlinePanel";
  70const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
  71
  72type Outline = OutlineItem<language::Anchor>;
  73
  74pub struct OutlinePanel {
  75    fs: Arc<dyn Fs>,
  76    width: Option<Pixels>,
  77    project: Model<Project>,
  78    active: bool,
  79    scroll_handle: UniformListScrollHandle,
  80    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
  81    focus_handle: FocusHandle,
  82    pending_serialization: Task<Option<()>>,
  83    fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,
  84    fs_entries: Vec<FsEntry>,
  85    fs_children_count: HashMap<WorktreeId, HashMap<Arc<Path>, FsChildren>>,
  86    collapsed_entries: HashSet<CollapsedEntry>,
  87    unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
  88    selected_entry: Option<EntryOwned>,
  89    active_item: Option<ActiveItem>,
  90    _subscriptions: Vec<Subscription>,
  91    updating_fs_entries: bool,
  92    fs_entries_update_task: Task<()>,
  93    cached_entries_update_task: Task<()>,
  94    outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>,
  95    excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
  96    cached_entries_with_depth: Vec<CachedEntry>,
  97    filter_editor: View<Editor>,
  98}
  99
 100#[derive(Debug, Clone, Copy, Default)]
 101struct FsChildren {
 102    files: usize,
 103    dirs: usize,
 104}
 105
 106impl FsChildren {
 107    fn may_be_fold_part(&self) -> bool {
 108        self.dirs == 0 || (self.dirs == 1 && self.files == 0)
 109    }
 110}
 111
 112#[derive(Clone, Debug)]
 113struct CachedEntry {
 114    depth: usize,
 115    string_match: Option<StringMatch>,
 116    entry: EntryOwned,
 117}
 118
 119#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 120enum CollapsedEntry {
 121    Dir(WorktreeId, ProjectEntryId),
 122    File(WorktreeId, BufferId),
 123    ExternalFile(BufferId),
 124    Excerpt(BufferId, ExcerptId),
 125}
 126
 127#[derive(Debug)]
 128struct Excerpt {
 129    range: ExcerptRange<language::Anchor>,
 130    outlines: ExcerptOutlines,
 131}
 132
 133impl Excerpt {
 134    fn invalidate_outlines(&mut self) {
 135        if let ExcerptOutlines::Outlines(valid_outlines) = &mut self.outlines {
 136            self.outlines = ExcerptOutlines::Invalidated(std::mem::take(valid_outlines));
 137        }
 138    }
 139
 140    fn iter_outlines(&self) -> impl Iterator<Item = &Outline> {
 141        match &self.outlines {
 142            ExcerptOutlines::Outlines(outlines) => outlines.iter(),
 143            ExcerptOutlines::Invalidated(outlines) => outlines.iter(),
 144            ExcerptOutlines::NotFetched => [].iter(),
 145        }
 146    }
 147
 148    fn should_fetch_outlines(&self) -> bool {
 149        match &self.outlines {
 150            ExcerptOutlines::Outlines(_) => false,
 151            ExcerptOutlines::Invalidated(_) => true,
 152            ExcerptOutlines::NotFetched => true,
 153        }
 154    }
 155}
 156
 157#[derive(Debug)]
 158enum ExcerptOutlines {
 159    Outlines(Vec<Outline>),
 160    Invalidated(Vec<Outline>),
 161    NotFetched,
 162}
 163
 164#[derive(Clone, Debug, PartialEq, Eq)]
 165enum EntryOwned {
 166    Entry(FsEntry),
 167    FoldedDirs(WorktreeId, Vec<Entry>),
 168    Excerpt(BufferId, ExcerptId, ExcerptRange<language::Anchor>),
 169    Outline(BufferId, ExcerptId, Outline),
 170}
 171
 172impl EntryOwned {
 173    fn to_ref_entry(&self) -> EntryRef<'_> {
 174        match self {
 175            Self::Entry(entry) => EntryRef::Entry(entry),
 176            Self::FoldedDirs(worktree_id, dirs) => EntryRef::FoldedDirs(*worktree_id, dirs),
 177            Self::Excerpt(buffer_id, excerpt_id, range) => {
 178                EntryRef::Excerpt(*buffer_id, *excerpt_id, range)
 179            }
 180            Self::Outline(buffer_id, excerpt_id, outline) => {
 181                EntryRef::Outline(*buffer_id, *excerpt_id, outline)
 182            }
 183        }
 184    }
 185}
 186
 187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 188enum EntryRef<'a> {
 189    Entry(&'a FsEntry),
 190    FoldedDirs(WorktreeId, &'a [Entry]),
 191    Excerpt(BufferId, ExcerptId, &'a ExcerptRange<language::Anchor>),
 192    Outline(BufferId, ExcerptId, &'a Outline),
 193}
 194
 195impl EntryRef<'_> {
 196    fn to_owned_entry(&self) -> EntryOwned {
 197        match self {
 198            &Self::Entry(entry) => EntryOwned::Entry(entry.clone()),
 199            &Self::FoldedDirs(worktree_id, dirs) => {
 200                EntryOwned::FoldedDirs(worktree_id, dirs.to_vec())
 201            }
 202            &Self::Excerpt(buffer_id, excerpt_id, range) => {
 203                EntryOwned::Excerpt(buffer_id, excerpt_id, range.clone())
 204            }
 205            &Self::Outline(buffer_id, excerpt_id, outline) => {
 206                EntryOwned::Outline(buffer_id, excerpt_id, outline.clone())
 207            }
 208        }
 209    }
 210}
 211
 212#[derive(Clone, Debug, Eq)]
 213enum FsEntry {
 214    ExternalFile(BufferId, Vec<ExcerptId>),
 215    Directory(WorktreeId, Entry),
 216    File(WorktreeId, Entry, BufferId, Vec<ExcerptId>),
 217}
 218
 219impl PartialEq for FsEntry {
 220    fn eq(&self, other: &Self) -> bool {
 221        match (self, other) {
 222            (Self::ExternalFile(id_a, _), Self::ExternalFile(id_b, _)) => id_a == id_b,
 223            (Self::Directory(id_a, entry_a), Self::Directory(id_b, entry_b)) => {
 224                id_a == id_b && entry_a.id == entry_b.id
 225            }
 226            (
 227                Self::File(worktree_a, entry_a, id_a, ..),
 228                Self::File(worktree_b, entry_b, id_b, ..),
 229            ) => worktree_a == worktree_b && entry_a.id == entry_b.id && id_a == id_b,
 230            _ => false,
 231        }
 232    }
 233}
 234
 235struct ActiveItem {
 236    item_id: EntityId,
 237    active_editor: WeakView<Editor>,
 238    _editor_subscrpiption: Subscription,
 239}
 240
 241#[derive(Debug)]
 242pub enum Event {
 243    Focus,
 244}
 245
 246#[derive(Serialize, Deserialize)]
 247struct SerializedOutlinePanel {
 248    width: Option<Pixels>,
 249    active: Option<bool>,
 250}
 251
 252pub fn init_settings(cx: &mut AppContext) {
 253    OutlinePanelSettings::register(cx);
 254}
 255
 256pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
 257    init_settings(cx);
 258    file_icons::init(assets, cx);
 259
 260    cx.observe_new_views(|workspace: &mut Workspace, _| {
 261        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
 262            workspace.toggle_panel_focus::<OutlinePanel>(cx);
 263        });
 264    })
 265    .detach();
 266}
 267
 268impl OutlinePanel {
 269    pub async fn load(
 270        workspace: WeakView<Workspace>,
 271        mut cx: AsyncWindowContext,
 272    ) -> anyhow::Result<View<Self>> {
 273        let serialized_panel = cx
 274            .background_executor()
 275            .spawn(async move { KEY_VALUE_STORE.read_kvp(OUTLINE_PANEL_KEY) })
 276            .await
 277            .context("loading outline panel")
 278            .log_err()
 279            .flatten()
 280            .map(|panel| serde_json::from_str::<SerializedOutlinePanel>(&panel))
 281            .transpose()
 282            .log_err()
 283            .flatten();
 284
 285        workspace.update(&mut cx, |workspace, cx| {
 286            let panel = Self::new(workspace, cx);
 287            if let Some(serialized_panel) = serialized_panel {
 288                panel.update(cx, |panel, cx| {
 289                    panel.width = serialized_panel.width.map(|px| px.round());
 290                    panel.active = serialized_panel.active.unwrap_or(false);
 291                    cx.notify();
 292                });
 293            }
 294            panel
 295        })
 296    }
 297
 298    fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 299        let project = workspace.project().clone();
 300        let outline_panel = cx.new_view(|cx| {
 301            let filter_editor = cx.new_view(|cx| {
 302                let mut editor = Editor::single_line(cx);
 303                editor.set_placeholder_text("Filter...", cx);
 304                editor
 305            });
 306            let filter_update_subscription =
 307                cx.subscribe(&filter_editor, |outline_panel: &mut Self, _, event, cx| {
 308                    if let editor::EditorEvent::BufferEdited = event {
 309                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
 310                    }
 311                });
 312
 313            let focus_handle = cx.focus_handle();
 314            let focus_subscription = cx.on_focus(&focus_handle, Self::focus_in);
 315            let workspace_subscription = cx.subscribe(
 316                &workspace
 317                    .weak_handle()
 318                    .upgrade()
 319                    .expect("have a &mut Workspace"),
 320                move |outline_panel, workspace, event, cx| {
 321                    if let workspace::Event::ActiveItemChanged = event {
 322                        if let Some(new_active_editor) = workspace
 323                            .read(cx)
 324                            .active_item(cx)
 325                            .and_then(|item| item.act_as::<Editor>(cx))
 326                        {
 327                            let active_editor_updated = outline_panel
 328                                .active_item
 329                                .as_ref()
 330                                .map_or(true, |active_item| {
 331                                    active_item.item_id != new_active_editor.item_id()
 332                                });
 333                            if active_editor_updated {
 334                                outline_panel.replace_visible_entries(new_active_editor, cx);
 335                            }
 336                        } else {
 337                            outline_panel.clear_previous(cx);
 338                            cx.notify();
 339                        }
 340                    }
 341                },
 342            );
 343
 344            let icons_subscription = cx.observe_global::<FileIcons>(|_, cx| {
 345                cx.notify();
 346            });
 347
 348            let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx);
 349            let settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
 350                let new_settings = *OutlinePanelSettings::get_global(cx);
 351                if outline_panel_settings != new_settings {
 352                    outline_panel_settings = new_settings;
 353                    cx.notify();
 354                }
 355            });
 356
 357            let mut outline_panel = Self {
 358                active: false,
 359                project: project.clone(),
 360                fs: workspace.app_state().fs.clone(),
 361                scroll_handle: UniformListScrollHandle::new(),
 362                focus_handle,
 363                filter_editor,
 364                fs_entries: Vec::new(),
 365                fs_entries_depth: HashMap::default(),
 366                fs_children_count: HashMap::default(),
 367                collapsed_entries: HashSet::default(),
 368                unfolded_dirs: HashMap::default(),
 369                selected_entry: None,
 370                context_menu: None,
 371                width: None,
 372                active_item: None,
 373                pending_serialization: Task::ready(None),
 374                updating_fs_entries: false,
 375                fs_entries_update_task: Task::ready(()),
 376                cached_entries_update_task: Task::ready(()),
 377                outline_fetch_tasks: HashMap::default(),
 378                excerpts: HashMap::default(),
 379                cached_entries_with_depth: Vec::new(),
 380                _subscriptions: vec![
 381                    settings_subscription,
 382                    icons_subscription,
 383                    focus_subscription,
 384                    workspace_subscription,
 385                    filter_update_subscription,
 386                ],
 387            };
 388            if let Some(editor) = workspace
 389                .active_item(cx)
 390                .and_then(|item| item.act_as::<Editor>(cx))
 391            {
 392                outline_panel.replace_visible_entries(editor, cx);
 393            }
 394            outline_panel
 395        });
 396
 397        outline_panel
 398    }
 399
 400    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 401        let width = self.width;
 402        let active = Some(self.active);
 403        self.pending_serialization = cx.background_executor().spawn(
 404            async move {
 405                KEY_VALUE_STORE
 406                    .write_kvp(
 407                        OUTLINE_PANEL_KEY.into(),
 408                        serde_json::to_string(&SerializedOutlinePanel { width, active })?,
 409                    )
 410                    .await?;
 411                anyhow::Ok(())
 412            }
 413            .log_err(),
 414        );
 415    }
 416
 417    fn dispatch_context(&self, _: &ViewContext<Self>) -> KeyContext {
 418        let mut dispatch_context = KeyContext::new_with_defaults();
 419        dispatch_context.add("OutlinePanel");
 420        dispatch_context.add("menu");
 421        dispatch_context
 422    }
 423
 424    fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext<Self>) {
 425        if let Some(EntryOwned::FoldedDirs(worktree_id, entries)) = &self.selected_entry {
 426            self.unfolded_dirs
 427                .entry(*worktree_id)
 428                .or_default()
 429                .extend(entries.iter().map(|entry| entry.id));
 430            self.update_cached_entries(None, cx);
 431        }
 432    }
 433
 434    fn fold_directory(&mut self, _: &FoldDirectory, cx: &mut ViewContext<Self>) {
 435        let (worktree_id, entry) = match &self.selected_entry {
 436            Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, entry))) => {
 437                (worktree_id, Some(entry))
 438            }
 439            Some(EntryOwned::FoldedDirs(worktree_id, entries)) => (worktree_id, entries.last()),
 440            _ => return,
 441        };
 442        let Some(entry) = entry else {
 443            return;
 444        };
 445        let unfolded_dirs = self.unfolded_dirs.get_mut(worktree_id);
 446        let worktree = self
 447            .project
 448            .read(cx)
 449            .worktree_for_id(*worktree_id, cx)
 450            .map(|w| w.read(cx).snapshot());
 451        let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
 452            return;
 453        };
 454
 455        unfolded_dirs.remove(&entry.id);
 456        self.update_cached_entries(None, cx);
 457    }
 458
 459    fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
 460        if let Some(selected_entry) = self.selected_entry.clone() {
 461            self.open_entry(&selected_entry, cx);
 462        }
 463    }
 464
 465    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
 466        if self.filter_editor.focus_handle(cx).is_focused(cx) {
 467            self.filter_editor.update(cx, |editor, cx| {
 468                if editor.buffer().read(cx).len(cx) > 0 {
 469                    editor.set_text("", cx);
 470                }
 471            });
 472        } else {
 473            cx.focus_view(&self.filter_editor);
 474        }
 475
 476        if self.context_menu.is_some() {
 477            self.context_menu.take();
 478            cx.notify();
 479        }
 480    }
 481
 482    fn open_entry(&mut self, entry: &EntryOwned, cx: &mut ViewContext<OutlinePanel>) {
 483        let Some(active_editor) = self
 484            .active_item
 485            .as_ref()
 486            .and_then(|item| item.active_editor.upgrade())
 487        else {
 488            return;
 489        };
 490        let active_multi_buffer = active_editor.read(cx).buffer().clone();
 491        let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
 492        let offset_from_top = if active_multi_buffer.read(cx).is_singleton() {
 493            Point::default()
 494        } else {
 495            Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32))
 496        };
 497
 498        self.toggle_expanded(entry, cx);
 499        match entry {
 500            EntryOwned::FoldedDirs(..) | EntryOwned::Entry(FsEntry::Directory(..)) => {}
 501            EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
 502                let scroll_target = multi_buffer_snapshot.excerpts().find_map(
 503                    |(excerpt_id, buffer_snapshot, excerpt_range)| {
 504                        if &buffer_snapshot.remote_id() == buffer_id {
 505                            multi_buffer_snapshot
 506                                .anchor_in_excerpt(excerpt_id, excerpt_range.context.start)
 507                        } else {
 508                            None
 509                        }
 510                    },
 511                );
 512                if let Some(anchor) = scroll_target {
 513                    self.selected_entry = Some(entry.clone());
 514                    active_editor.update(cx, |editor, cx| {
 515                        editor.set_scroll_anchor(
 516                            ScrollAnchor {
 517                                offset: offset_from_top,
 518                                anchor,
 519                            },
 520                            cx,
 521                        );
 522                    })
 523                }
 524            }
 525            EntryOwned::Entry(FsEntry::File(_, file_entry, ..)) => {
 526                let scroll_target = self
 527                    .project
 528                    .update(cx, |project, cx| {
 529                        project
 530                            .path_for_entry(file_entry.id, cx)
 531                            .and_then(|path| project.get_open_buffer(&path, cx))
 532                    })
 533                    .map(|buffer| {
 534                        active_multi_buffer
 535                            .read(cx)
 536                            .excerpts_for_buffer(&buffer, cx)
 537                    })
 538                    .and_then(|excerpts| {
 539                        let (excerpt_id, excerpt_range) = excerpts.first()?;
 540                        multi_buffer_snapshot
 541                            .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
 542                    });
 543                if let Some(anchor) = scroll_target {
 544                    self.selected_entry = Some(entry.clone());
 545                    active_editor.update(cx, |editor, cx| {
 546                        editor.set_scroll_anchor(
 547                            ScrollAnchor {
 548                                offset: offset_from_top,
 549                                anchor,
 550                            },
 551                            cx,
 552                        );
 553                    })
 554                }
 555            }
 556            EntryOwned::Outline(_, excerpt_id, outline) => {
 557                let scroll_target = multi_buffer_snapshot
 558                    .anchor_in_excerpt(*excerpt_id, outline.range.start)
 559                    .or_else(|| {
 560                        multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, outline.range.end)
 561                    });
 562                if let Some(anchor) = scroll_target {
 563                    self.selected_entry = Some(entry.clone());
 564                    active_editor.update(cx, |editor, cx| {
 565                        editor.set_scroll_anchor(
 566                            ScrollAnchor {
 567                                offset: Point::default(),
 568                                anchor,
 569                            },
 570                            cx,
 571                        );
 572                    })
 573                }
 574            }
 575            EntryOwned::Excerpt(_, excerpt_id, excerpt_range) => {
 576                let scroll_target = multi_buffer_snapshot
 577                    .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start);
 578                if let Some(anchor) = scroll_target {
 579                    self.selected_entry = Some(entry.clone());
 580                    active_editor.update(cx, |editor, cx| {
 581                        editor.set_scroll_anchor(
 582                            ScrollAnchor {
 583                                offset: Point::default(),
 584                                anchor,
 585                            },
 586                            cx,
 587                        );
 588                    })
 589                }
 590            }
 591        }
 592    }
 593
 594    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
 595        if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
 596            self.cached_entries_with_depth
 597                .iter()
 598                .map(|cached_entry| &cached_entry.entry)
 599                .skip_while(|entry| entry != &&selected_entry)
 600                .skip(1)
 601                .next()
 602                .cloned()
 603        }) {
 604            self.selected_entry = Some(entry_to_select);
 605            self.autoscroll(cx);
 606            cx.notify();
 607        } else {
 608            self.select_first(&SelectFirst {}, cx)
 609        }
 610    }
 611
 612    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
 613        if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
 614            self.cached_entries_with_depth
 615                .iter()
 616                .rev()
 617                .map(|cached_entry| &cached_entry.entry)
 618                .skip_while(|entry| entry != &&selected_entry)
 619                .skip(1)
 620                .next()
 621                .cloned()
 622        }) {
 623            self.selected_entry = Some(entry_to_select);
 624            self.autoscroll(cx);
 625            cx.notify();
 626        } else {
 627            self.select_first(&SelectFirst {}, cx)
 628        }
 629    }
 630
 631    fn select_parent(&mut self, _: &SelectParent, cx: &mut ViewContext<Self>) {
 632        if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
 633            let mut previous_entries = self
 634                .cached_entries_with_depth
 635                .iter()
 636                .rev()
 637                .map(|cached_entry| &cached_entry.entry)
 638                .skip_while(|entry| entry != &&selected_entry)
 639                .skip(1);
 640            match &selected_entry {
 641                EntryOwned::Entry(fs_entry) => match fs_entry {
 642                    FsEntry::ExternalFile(..) => None,
 643                    FsEntry::File(worktree_id, entry, ..)
 644                    | FsEntry::Directory(worktree_id, entry) => {
 645                        entry.path.parent().and_then(|parent_path| {
 646                            previous_entries.find(|entry| match entry {
 647                                EntryOwned::Entry(FsEntry::Directory(
 648                                    dir_worktree_id,
 649                                    dir_entry,
 650                                )) => {
 651                                    dir_worktree_id == worktree_id
 652                                        && dir_entry.path.as_ref() == parent_path
 653                                }
 654                                EntryOwned::FoldedDirs(dirs_worktree_id, dirs) => {
 655                                    dirs_worktree_id == worktree_id
 656                                        && dirs
 657                                            .first()
 658                                            .map_or(false, |dir| dir.path.as_ref() == parent_path)
 659                                }
 660                                _ => false,
 661                            })
 662                        })
 663                    }
 664                },
 665                EntryOwned::FoldedDirs(worktree_id, entries) => entries
 666                    .first()
 667                    .and_then(|entry| entry.path.parent())
 668                    .and_then(|parent_path| {
 669                        previous_entries.find(|entry| {
 670                            if let EntryOwned::Entry(FsEntry::Directory(
 671                                dir_worktree_id,
 672                                dir_entry,
 673                            )) = entry
 674                            {
 675                                dir_worktree_id == worktree_id
 676                                    && dir_entry.path.as_ref() == parent_path
 677                            } else {
 678                                false
 679                            }
 680                        })
 681                    }),
 682                EntryOwned::Excerpt(excerpt_buffer_id, excerpt_id, _) => {
 683                    previous_entries.find(|entry| match entry {
 684                        EntryOwned::Entry(FsEntry::File(_, _, file_buffer_id, file_excerpts)) => {
 685                            file_buffer_id == excerpt_buffer_id
 686                                && file_excerpts.contains(&excerpt_id)
 687                        }
 688                        EntryOwned::Entry(FsEntry::ExternalFile(file_buffer_id, file_excerpts)) => {
 689                            file_buffer_id == excerpt_buffer_id
 690                                && file_excerpts.contains(&excerpt_id)
 691                        }
 692                        _ => false,
 693                    })
 694                }
 695                EntryOwned::Outline(outline_buffer_id, outline_excerpt_id, _) => previous_entries
 696                    .find(|entry| {
 697                        if let EntryOwned::Excerpt(excerpt_buffer_id, excerpt_id, _) = entry {
 698                            outline_buffer_id == excerpt_buffer_id
 699                                && outline_excerpt_id == excerpt_id
 700                        } else {
 701                            false
 702                        }
 703                    }),
 704            }
 705        }) {
 706            self.selected_entry = Some(entry_to_select.clone());
 707            self.autoscroll(cx);
 708            cx.notify();
 709        } else {
 710            self.select_first(&SelectFirst {}, cx);
 711        }
 712    }
 713
 714    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
 715        if let Some(first_entry) = self.cached_entries_with_depth.iter().next() {
 716            self.selected_entry = Some(first_entry.entry.clone());
 717            self.autoscroll(cx);
 718            cx.notify();
 719        }
 720    }
 721
 722    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
 723        if let Some(new_selection) = self
 724            .cached_entries_with_depth
 725            .iter()
 726            .rev()
 727            .map(|cached_entry| &cached_entry.entry)
 728            .next()
 729        {
 730            self.selected_entry = Some(new_selection.clone());
 731            self.autoscroll(cx);
 732            cx.notify();
 733        }
 734    }
 735
 736    fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
 737        if let Some(selected_entry) = self.selected_entry.clone() {
 738            let index = self
 739                .cached_entries_with_depth
 740                .iter()
 741                .position(|cached_entry| cached_entry.entry == selected_entry);
 742            if let Some(index) = index {
 743                self.scroll_handle.scroll_to_item(index);
 744                cx.notify();
 745            }
 746        }
 747    }
 748
 749    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 750        if !self.focus_handle.contains_focused(cx) {
 751            cx.emit(Event::Focus);
 752        }
 753    }
 754
 755    fn deploy_context_menu(
 756        &mut self,
 757        position: Point<Pixels>,
 758        entry: EntryRef<'_>,
 759        cx: &mut ViewContext<Self>,
 760    ) {
 761        self.selected_entry = Some(entry.to_owned_entry());
 762        let is_root = match entry {
 763            EntryRef::Entry(FsEntry::File(worktree_id, entry, ..))
 764            | EntryRef::Entry(FsEntry::Directory(worktree_id, entry)) => self
 765                .project
 766                .read(cx)
 767                .worktree_for_id(*worktree_id, cx)
 768                .map(|worktree| {
 769                    worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
 770                })
 771                .unwrap_or(false),
 772            EntryRef::FoldedDirs(worktree_id, entries) => entries
 773                .first()
 774                .and_then(|entry| {
 775                    self.project
 776                        .read(cx)
 777                        .worktree_for_id(worktree_id, cx)
 778                        .map(|worktree| {
 779                            worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
 780                        })
 781                })
 782                .unwrap_or(false),
 783            EntryRef::Entry(FsEntry::ExternalFile(..)) => false,
 784            EntryRef::Excerpt(..) => {
 785                cx.notify();
 786                return;
 787            }
 788            EntryRef::Outline(..) => {
 789                cx.notify();
 790                return;
 791            }
 792        };
 793        let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
 794        let is_foldable = auto_fold_dirs && !is_root && self.is_foldable(entry);
 795        let is_unfoldable = auto_fold_dirs && !is_root && self.is_unfoldable(entry);
 796
 797        let context_menu = ContextMenu::build(cx, |menu, _| {
 798            menu.context(self.focus_handle.clone())
 799                .when(cfg!(target_os = "macos"), |menu| {
 800                    menu.action("Reveal in Finder", Box::new(RevealInFileManager))
 801                })
 802                .when(cfg!(not(target_os = "macos")), |menu| {
 803                    menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
 804                })
 805                .action("Open in Terminal", Box::new(OpenInTerminal))
 806                .when(is_unfoldable, |menu| {
 807                    menu.action("Unfold Directory", Box::new(UnfoldDirectory))
 808                })
 809                .when(is_foldable, |menu| {
 810                    menu.action("Fold Directory", Box::new(FoldDirectory))
 811                })
 812                .separator()
 813                .action("Copy Path", Box::new(CopyPath))
 814                .action("Copy Relative Path", Box::new(CopyRelativePath))
 815        });
 816        cx.focus_view(&context_menu);
 817        let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| {
 818            outline_panel.context_menu.take();
 819            cx.notify();
 820        });
 821        self.context_menu = Some((context_menu, position, subscription));
 822        cx.notify();
 823    }
 824
 825    fn is_unfoldable(&self, entry: EntryRef) -> bool {
 826        matches!(entry, EntryRef::FoldedDirs(..))
 827    }
 828
 829    fn is_foldable(&self, entry: EntryRef) -> bool {
 830        let (directory_worktree, directory_entry) = match entry {
 831            EntryRef::Entry(FsEntry::Directory(directory_worktree, directory_entry)) => {
 832                (*directory_worktree, Some(directory_entry))
 833            }
 834            _ => return false,
 835        };
 836        let Some(directory_entry) = directory_entry else {
 837            return false;
 838        };
 839
 840        if self
 841            .unfolded_dirs
 842            .get(&directory_worktree)
 843            .map_or(true, |unfolded_dirs| {
 844                !unfolded_dirs.contains(&directory_entry.id)
 845            })
 846        {
 847            return false;
 848        }
 849
 850        let children = self
 851            .fs_children_count
 852            .get(&directory_worktree)
 853            .and_then(|entries| entries.get(&directory_entry.path))
 854            .copied()
 855            .unwrap_or_default();
 856
 857        children.may_be_fold_part() && children.dirs > 0
 858    }
 859
 860    fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
 861        let entry_to_expand = match &self.selected_entry {
 862            Some(EntryOwned::FoldedDirs(worktree_id, dir_entries)) => dir_entries
 863                .last()
 864                .map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id)),
 865            Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry))) => {
 866                Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
 867            }
 868            Some(EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => {
 869                Some(CollapsedEntry::File(*worktree_id, *buffer_id))
 870            }
 871            Some(EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => {
 872                Some(CollapsedEntry::ExternalFile(*buffer_id))
 873            }
 874            Some(EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => {
 875                Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
 876            }
 877            None | Some(EntryOwned::Outline(..)) => None,
 878        };
 879        let Some(collapsed_entry) = entry_to_expand else {
 880            return;
 881        };
 882        let expanded = self.collapsed_entries.remove(&collapsed_entry);
 883        if expanded {
 884            if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
 885                self.project.update(cx, |project, cx| {
 886                    project.expand_entry(worktree_id, dir_entry_id, cx);
 887                });
 888            }
 889            self.update_cached_entries(None, cx);
 890        } else {
 891            self.select_next(&SelectNext, cx)
 892        }
 893    }
 894
 895    fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
 896        match &self.selected_entry {
 897            Some(
 898                dir_entry @ EntryOwned::Entry(FsEntry::Directory(worktree_id, selected_dir_entry)),
 899            ) => {
 900                self.collapsed_entries
 901                    .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id));
 902                self.selected_entry = Some(dir_entry.clone());
 903                self.update_cached_entries(None, cx);
 904            }
 905            Some(file_entry @ EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => {
 906                self.collapsed_entries
 907                    .insert(CollapsedEntry::File(*worktree_id, *buffer_id));
 908                self.selected_entry = Some(file_entry.clone());
 909                self.update_cached_entries(None, cx);
 910            }
 911            Some(file_entry @ EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => {
 912                self.collapsed_entries
 913                    .insert(CollapsedEntry::ExternalFile(*buffer_id));
 914                self.selected_entry = Some(file_entry.clone());
 915                self.update_cached_entries(None, cx);
 916            }
 917            Some(dirs_entry @ EntryOwned::FoldedDirs(worktree_id, dir_entries)) => {
 918                if let Some(dir_entry) = dir_entries.last() {
 919                    if self
 920                        .collapsed_entries
 921                        .insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
 922                    {
 923                        self.selected_entry = Some(dirs_entry.clone());
 924                        self.update_cached_entries(None, cx);
 925                    }
 926                }
 927            }
 928            Some(excerpt_entry @ EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => {
 929                if self
 930                    .collapsed_entries
 931                    .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
 932                {
 933                    self.selected_entry = Some(excerpt_entry.clone());
 934                    self.update_cached_entries(None, cx);
 935                }
 936            }
 937            None | Some(EntryOwned::Outline(..)) => {}
 938        }
 939    }
 940
 941    pub fn expand_all_entries(&mut self, _: &ExpandAllEntries, cx: &mut ViewContext<Self>) {
 942        let expanded_entries =
 943            self.fs_entries
 944                .iter()
 945                .fold(HashSet::default(), |mut entries, fs_entry| {
 946                    match fs_entry {
 947                        FsEntry::ExternalFile(buffer_id, _) => {
 948                            entries.insert(CollapsedEntry::ExternalFile(*buffer_id));
 949                            entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
 950                                |excerpts| {
 951                                    excerpts.iter().map(|(excerpt_id, _)| {
 952                                        CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)
 953                                    })
 954                                },
 955                            ));
 956                        }
 957                        FsEntry::Directory(worktree_id, entry) => {
 958                            entries.insert(CollapsedEntry::Dir(*worktree_id, entry.id));
 959                        }
 960                        FsEntry::File(worktree_id, _, buffer_id, _) => {
 961                            entries.insert(CollapsedEntry::File(*worktree_id, *buffer_id));
 962                            entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
 963                                |excerpts| {
 964                                    excerpts.iter().map(|(excerpt_id, _)| {
 965                                        CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)
 966                                    })
 967                                },
 968                            ));
 969                        }
 970                    }
 971                    entries
 972                });
 973        self.collapsed_entries
 974            .retain(|entry| !expanded_entries.contains(entry));
 975        self.update_cached_entries(None, cx);
 976    }
 977
 978    pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
 979        let new_entries = self
 980            .cached_entries_with_depth
 981            .iter()
 982            .flat_map(|cached_entry| match &cached_entry.entry {
 983                EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => {
 984                    Some(CollapsedEntry::Dir(*worktree_id, entry.id))
 985                }
 986                EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
 987                    Some(CollapsedEntry::File(*worktree_id, *buffer_id))
 988                }
 989                EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
 990                    Some(CollapsedEntry::ExternalFile(*buffer_id))
 991                }
 992                EntryOwned::FoldedDirs(worktree_id, entries) => {
 993                    Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id))
 994                }
 995                EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
 996                    Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
 997                }
 998                EntryOwned::Outline(..) => None,
 999            })
1000            .collect::<Vec<_>>();
1001        self.collapsed_entries.extend(new_entries);
1002        self.update_cached_entries(None, cx);
1003    }
1004
1005    fn toggle_expanded(&mut self, entry: &EntryOwned, cx: &mut ViewContext<Self>) {
1006        match entry {
1007            EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry)) => {
1008                let entry_id = dir_entry.id;
1009                let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1010                if self.collapsed_entries.remove(&collapsed_entry) {
1011                    self.project
1012                        .update(cx, |project, cx| {
1013                            project.expand_entry(*worktree_id, entry_id, cx)
1014                        })
1015                        .unwrap_or_else(|| Task::ready(Ok(())))
1016                        .detach_and_log_err(cx);
1017                } else {
1018                    self.collapsed_entries.insert(collapsed_entry);
1019                }
1020            }
1021            EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1022                let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
1023                if !self.collapsed_entries.remove(&collapsed_entry) {
1024                    self.collapsed_entries.insert(collapsed_entry);
1025                }
1026            }
1027            EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
1028                let collapsed_entry = CollapsedEntry::ExternalFile(*buffer_id);
1029                if !self.collapsed_entries.remove(&collapsed_entry) {
1030                    self.collapsed_entries.insert(collapsed_entry);
1031                }
1032            }
1033            EntryOwned::FoldedDirs(worktree_id, dir_entries) => {
1034                if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) {
1035                    let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1036                    if self.collapsed_entries.remove(&collapsed_entry) {
1037                        self.project
1038                            .update(cx, |project, cx| {
1039                                project.expand_entry(*worktree_id, entry_id, cx)
1040                            })
1041                            .unwrap_or_else(|| Task::ready(Ok(())))
1042                            .detach_and_log_err(cx);
1043                    } else {
1044                        self.collapsed_entries.insert(collapsed_entry);
1045                    }
1046                }
1047            }
1048            EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
1049                let collapsed_entry = CollapsedEntry::Excerpt(*buffer_id, *excerpt_id);
1050                if !self.collapsed_entries.remove(&collapsed_entry) {
1051                    self.collapsed_entries.insert(collapsed_entry);
1052                }
1053            }
1054            EntryOwned::Outline(..) => return,
1055        }
1056
1057        self.selected_entry = Some(entry.clone());
1058        self.update_cached_entries(None, cx);
1059    }
1060
1061    fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
1062        if let Some(clipboard_text) = self
1063            .selected_entry
1064            .as_ref()
1065            .and_then(|entry| self.abs_path(&entry, cx))
1066            .map(|p| p.to_string_lossy().to_string())
1067        {
1068            cx.write_to_clipboard(ClipboardItem::new(clipboard_text));
1069        }
1070    }
1071
1072    fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
1073        if let Some(clipboard_text) = self
1074            .selected_entry
1075            .as_ref()
1076            .and_then(|entry| match entry {
1077                EntryOwned::Entry(entry) => self.relative_path(&entry, cx),
1078                EntryOwned::FoldedDirs(_, dirs) => dirs.last().map(|entry| entry.path.clone()),
1079                EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
1080            })
1081            .map(|p| p.to_string_lossy().to_string())
1082        {
1083            cx.write_to_clipboard(ClipboardItem::new(clipboard_text));
1084        }
1085    }
1086
1087    fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
1088        if let Some(abs_path) = self
1089            .selected_entry
1090            .as_ref()
1091            .and_then(|entry| self.abs_path(&entry, cx))
1092        {
1093            cx.reveal_path(&abs_path);
1094        }
1095    }
1096
1097    fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
1098        let selected_entry = self.selected_entry.as_ref();
1099        let abs_path = selected_entry.and_then(|entry| self.abs_path(&entry, cx));
1100        let working_directory = if let (
1101            Some(abs_path),
1102            Some(EntryOwned::Entry(FsEntry::File(..) | FsEntry::ExternalFile(..))),
1103        ) = (&abs_path, selected_entry)
1104        {
1105            abs_path.parent().map(|p| p.to_owned())
1106        } else {
1107            abs_path
1108        };
1109
1110        if let Some(working_directory) = working_directory {
1111            cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone())
1112        }
1113    }
1114
1115    fn reveal_entry_for_selection(
1116        &mut self,
1117        editor: &View<Editor>,
1118        cx: &mut ViewContext<'_, Self>,
1119    ) {
1120        if !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
1121            return;
1122        }
1123        let Some(entry_with_selection) = self.location_for_editor_selection(editor, cx) else {
1124            self.selected_entry = None;
1125            cx.notify();
1126            return;
1127        };
1128        let related_buffer_entry = match entry_with_selection {
1129            EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1130                let project = self.project.read(cx);
1131                let entry_id = project
1132                    .buffer_for_id(buffer_id)
1133                    .and_then(|buffer| buffer.read(cx).entry_id(cx));
1134                project
1135                    .worktree_for_id(worktree_id, cx)
1136                    .zip(entry_id)
1137                    .and_then(|(worktree, entry_id)| {
1138                        let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1139                        Some((worktree, entry))
1140                    })
1141            }
1142            EntryOwned::Outline(buffer_id, excerpt_id, _)
1143            | EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
1144                self.collapsed_entries
1145                    .remove(&CollapsedEntry::ExternalFile(buffer_id));
1146                self.collapsed_entries
1147                    .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1148                let project = self.project.read(cx);
1149                let entry_id = project
1150                    .buffer_for_id(buffer_id)
1151                    .and_then(|buffer| buffer.read(cx).entry_id(cx));
1152
1153                entry_id.and_then(|entry_id| {
1154                    project
1155                        .worktree_for_entry(entry_id, cx)
1156                        .and_then(|worktree| {
1157                            let worktree_id = worktree.read(cx).id();
1158                            self.collapsed_entries
1159                                .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1160                            let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1161                            Some((worktree, entry))
1162                        })
1163                })
1164            }
1165            EntryOwned::Entry(FsEntry::ExternalFile(..)) => None,
1166            _ => return,
1167        };
1168        if let Some((worktree, buffer_entry)) = related_buffer_entry {
1169            let worktree_id = worktree.read(cx).id();
1170            let mut dirs_to_expand = Vec::new();
1171            {
1172                let mut traversal = worktree.read(cx).traverse_from_path(
1173                    true,
1174                    true,
1175                    true,
1176                    buffer_entry.path.as_ref(),
1177                );
1178                let mut current_entry = buffer_entry;
1179                loop {
1180                    if current_entry.is_dir() {
1181                        if self
1182                            .collapsed_entries
1183                            .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
1184                        {
1185                            dirs_to_expand.push(current_entry.id);
1186                        }
1187                    }
1188
1189                    if traversal.back_to_parent() {
1190                        if let Some(parent_entry) = traversal.entry() {
1191                            current_entry = parent_entry.clone();
1192                            continue;
1193                        }
1194                    }
1195                    break;
1196                }
1197            }
1198            for dir_to_expand in dirs_to_expand {
1199                self.project
1200                    .update(cx, |project, cx| {
1201                        project.expand_entry(worktree_id, dir_to_expand, cx)
1202                    })
1203                    .unwrap_or_else(|| Task::ready(Ok(())))
1204                    .detach_and_log_err(cx)
1205            }
1206        }
1207
1208        self.selected_entry = Some(entry_with_selection);
1209        self.update_cached_entries(None, cx);
1210        self.autoscroll(cx);
1211    }
1212
1213    fn render_excerpt(
1214        &self,
1215        buffer_id: BufferId,
1216        excerpt_id: ExcerptId,
1217        range: &ExcerptRange<language::Anchor>,
1218        depth: usize,
1219        cx: &mut ViewContext<OutlinePanel>,
1220    ) -> Option<Stateful<Div>> {
1221        let item_id = ElementId::from(excerpt_id.to_proto() as usize);
1222        let is_active = match &self.selected_entry {
1223            Some(EntryOwned::Excerpt(selected_buffer_id, selected_excerpt_id, _)) => {
1224                selected_buffer_id == &buffer_id && selected_excerpt_id == &excerpt_id
1225            }
1226            _ => false,
1227        };
1228        let has_outlines = self
1229            .excerpts
1230            .get(&buffer_id)
1231            .and_then(|excerpts| match &excerpts.get(&excerpt_id)?.outlines {
1232                ExcerptOutlines::Outlines(outlines) => Some(outlines),
1233                ExcerptOutlines::Invalidated(outlines) => Some(outlines),
1234                ExcerptOutlines::NotFetched => None,
1235            })
1236            .map_or(false, |outlines| !outlines.is_empty());
1237        let is_expanded = !self
1238            .collapsed_entries
1239            .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1240        let color = entry_git_aware_label_color(None, false, is_active);
1241        let icon = if has_outlines {
1242            FileIcons::get_chevron_icon(is_expanded, cx)
1243                .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
1244        } else {
1245            None
1246        }
1247        .unwrap_or_else(empty_icon);
1248
1249        let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
1250        let excerpt_range = range.context.to_point(&buffer_snapshot);
1251        let label_element = Label::new(format!(
1252            "Lines {}-{}",
1253            excerpt_range.start.row + 1,
1254            excerpt_range.end.row + 1,
1255        ))
1256        .single_line()
1257        .color(color)
1258        .into_any_element();
1259
1260        Some(self.entry_element(
1261            EntryRef::Excerpt(buffer_id, excerpt_id, range),
1262            item_id,
1263            depth,
1264            Some(icon),
1265            is_active,
1266            label_element,
1267            cx,
1268        ))
1269    }
1270
1271    fn render_outline(
1272        &self,
1273        buffer_id: BufferId,
1274        excerpt_id: ExcerptId,
1275        rendered_outline: &Outline,
1276        depth: usize,
1277        string_match: Option<&StringMatch>,
1278        cx: &mut ViewContext<Self>,
1279    ) -> Stateful<Div> {
1280        let (item_id, label_element) = (
1281            ElementId::from(SharedString::from(format!(
1282                "{buffer_id:?}|{excerpt_id:?}{:?}|{:?}",
1283                rendered_outline.range, &rendered_outline.text,
1284            ))),
1285            language::render_item(
1286                &rendered_outline,
1287                string_match
1288                    .map(|string_match| string_match.ranges().collect::<Vec<_>>())
1289                    .unwrap_or_default(),
1290                cx,
1291            )
1292            .into_any_element(),
1293        );
1294        let is_active = match &self.selected_entry {
1295            Some(EntryOwned::Outline(selected_buffer_id, selected_excerpt_id, selected_entry)) => {
1296                selected_buffer_id == &buffer_id
1297                    && selected_excerpt_id == &excerpt_id
1298                    && selected_entry == rendered_outline
1299            }
1300            _ => false,
1301        };
1302        let icon = if self.is_singleton_active(cx) {
1303            None
1304        } else {
1305            Some(empty_icon())
1306        };
1307        self.entry_element(
1308            EntryRef::Outline(buffer_id, excerpt_id, rendered_outline),
1309            item_id,
1310            depth,
1311            icon,
1312            is_active,
1313            label_element,
1314            cx,
1315        )
1316    }
1317
1318    fn render_entry(
1319        &self,
1320        rendered_entry: &FsEntry,
1321        depth: usize,
1322        string_match: Option<&StringMatch>,
1323        cx: &mut ViewContext<Self>,
1324    ) -> Stateful<Div> {
1325        let settings = OutlinePanelSettings::get_global(cx);
1326        let is_active = match &self.selected_entry {
1327            Some(EntryOwned::Entry(selected_entry)) => selected_entry == rendered_entry,
1328            _ => false,
1329        };
1330        let (item_id, label_element, icon) = match rendered_entry {
1331            FsEntry::File(worktree_id, entry, ..) => {
1332                let name = self.entry_name(worktree_id, entry, cx);
1333                let color =
1334                    entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
1335                let icon = if settings.file_icons {
1336                    FileIcons::get_icon(&entry.path, cx)
1337                        .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
1338                } else {
1339                    None
1340                };
1341                (
1342                    ElementId::from(entry.id.to_proto() as usize),
1343                    HighlightedLabel::new(
1344                        name,
1345                        string_match
1346                            .map(|string_match| string_match.positions.clone())
1347                            .unwrap_or_default(),
1348                    )
1349                    .color(color)
1350                    .into_any_element(),
1351                    icon.unwrap_or_else(empty_icon),
1352                )
1353            }
1354            FsEntry::Directory(worktree_id, entry) => {
1355                let name = self.entry_name(worktree_id, entry, cx);
1356
1357                let is_expanded = !self
1358                    .collapsed_entries
1359                    .contains(&CollapsedEntry::Dir(*worktree_id, entry.id));
1360                let color =
1361                    entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
1362                let icon = if settings.folder_icons {
1363                    FileIcons::get_folder_icon(is_expanded, cx)
1364                } else {
1365                    FileIcons::get_chevron_icon(is_expanded, cx)
1366                }
1367                .map(Icon::from_path)
1368                .map(|icon| icon.color(color).into_any_element());
1369                (
1370                    ElementId::from(entry.id.to_proto() as usize),
1371                    HighlightedLabel::new(
1372                        name,
1373                        string_match
1374                            .map(|string_match| string_match.positions.clone())
1375                            .unwrap_or_default(),
1376                    )
1377                    .color(color)
1378                    .into_any_element(),
1379                    icon.unwrap_or_else(empty_icon),
1380                )
1381            }
1382            FsEntry::ExternalFile(buffer_id, ..) => {
1383                let color = entry_label_color(is_active);
1384                let (icon, name) = match self.buffer_snapshot_for_id(*buffer_id, cx) {
1385                    Some(buffer_snapshot) => match buffer_snapshot.file() {
1386                        Some(file) => {
1387                            let path = file.path();
1388                            let icon = if settings.file_icons {
1389                                FileIcons::get_icon(path.as_ref(), cx)
1390                            } else {
1391                                None
1392                            }
1393                            .map(Icon::from_path)
1394                            .map(|icon| icon.color(color).into_any_element());
1395                            (icon, file_name(path.as_ref()))
1396                        }
1397                        None => (None, "Untitled".to_string()),
1398                    },
1399                    None => (None, "Unknown buffer".to_string()),
1400                };
1401                (
1402                    ElementId::from(buffer_id.to_proto() as usize),
1403                    HighlightedLabel::new(
1404                        name,
1405                        string_match
1406                            .map(|string_match| string_match.positions.clone())
1407                            .unwrap_or_default(),
1408                    )
1409                    .color(color)
1410                    .into_any_element(),
1411                    icon.unwrap_or_else(empty_icon),
1412                )
1413            }
1414        };
1415
1416        self.entry_element(
1417            EntryRef::Entry(rendered_entry),
1418            item_id,
1419            depth,
1420            Some(icon),
1421            is_active,
1422            label_element,
1423            cx,
1424        )
1425    }
1426
1427    fn render_folded_dirs(
1428        &self,
1429        worktree_id: WorktreeId,
1430        dir_entries: &[Entry],
1431        depth: usize,
1432        string_match: Option<&StringMatch>,
1433        cx: &mut ViewContext<OutlinePanel>,
1434    ) -> Stateful<Div> {
1435        let settings = OutlinePanelSettings::get_global(cx);
1436        let is_active = match &self.selected_entry {
1437            Some(EntryOwned::FoldedDirs(selected_worktree_id, selected_entries)) => {
1438                selected_worktree_id == &worktree_id && selected_entries == dir_entries
1439            }
1440            _ => false,
1441        };
1442        let (item_id, label_element, icon) = {
1443            let name = self.dir_names_string(dir_entries, worktree_id, cx);
1444
1445            let is_expanded = dir_entries.iter().all(|dir| {
1446                !self
1447                    .collapsed_entries
1448                    .contains(&CollapsedEntry::Dir(worktree_id, dir.id))
1449            });
1450            let is_ignored = dir_entries.iter().any(|entry| entry.is_ignored);
1451            let git_status = dir_entries.first().and_then(|entry| entry.git_status);
1452            let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
1453            let icon = if settings.folder_icons {
1454                FileIcons::get_folder_icon(is_expanded, cx)
1455            } else {
1456                FileIcons::get_chevron_icon(is_expanded, cx)
1457            }
1458            .map(Icon::from_path)
1459            .map(|icon| icon.color(color).into_any_element());
1460            (
1461                ElementId::from(
1462                    dir_entries
1463                        .last()
1464                        .map(|entry| entry.id.to_proto())
1465                        .unwrap_or_else(|| worktree_id.to_proto()) as usize,
1466                ),
1467                HighlightedLabel::new(
1468                    name,
1469                    string_match
1470                        .map(|string_match| string_match.positions.clone())
1471                        .unwrap_or_default(),
1472                )
1473                .color(color)
1474                .into_any_element(),
1475                icon.unwrap_or_else(empty_icon),
1476            )
1477        };
1478
1479        self.entry_element(
1480            EntryRef::FoldedDirs(worktree_id, dir_entries),
1481            item_id,
1482            depth,
1483            Some(icon),
1484            is_active,
1485            label_element,
1486            cx,
1487        )
1488    }
1489
1490    #[allow(clippy::too_many_arguments)]
1491    fn entry_element(
1492        &self,
1493        rendered_entry: EntryRef<'_>,
1494        item_id: ElementId,
1495        depth: usize,
1496        icon_element: Option<AnyElement>,
1497        is_active: bool,
1498        label_element: gpui::AnyElement,
1499        cx: &mut ViewContext<OutlinePanel>,
1500    ) -> Stateful<Div> {
1501        let settings = OutlinePanelSettings::get_global(cx);
1502        let rendered_entry = rendered_entry.to_owned_entry();
1503        div()
1504            .text_ui(cx)
1505            .id(item_id.clone())
1506            .child(
1507                ListItem::new(item_id)
1508                    .indent_level(depth)
1509                    .indent_step_size(px(settings.indent_size))
1510                    .selected(is_active)
1511                    .when_some(icon_element, |list_item, icon_element| {
1512                        list_item.child(h_flex().child(icon_element))
1513                    })
1514                    .child(h_flex().h_6().child(label_element).ml_1())
1515                    .on_click({
1516                        let clicked_entry = rendered_entry.clone();
1517                        cx.listener(move |outline_panel, event: &gpui::ClickEvent, cx| {
1518                            if event.down.button == MouseButton::Right || event.down.first_mouse {
1519                                return;
1520                            }
1521                            outline_panel.open_entry(&clicked_entry, cx);
1522                        })
1523                    })
1524                    .on_secondary_mouse_down(cx.listener(
1525                        move |outline_panel, event: &MouseDownEvent, cx| {
1526                            // Stop propagation to prevent the catch-all context menu for the project
1527                            // panel from being deployed.
1528                            cx.stop_propagation();
1529                            outline_panel.deploy_context_menu(
1530                                event.position,
1531                                rendered_entry.to_ref_entry(),
1532                                cx,
1533                            )
1534                        },
1535                    )),
1536            )
1537            .border_1()
1538            .border_r_2()
1539            .rounded_none()
1540            .hover(|style| {
1541                if is_active {
1542                    style
1543                } else {
1544                    let hover_color = cx.theme().colors().ghost_element_hover;
1545                    style.bg(hover_color).border_color(hover_color)
1546                }
1547            })
1548            .when(is_active && self.focus_handle.contains_focused(cx), |div| {
1549                div.border_color(Color::Selected.color(cx))
1550            })
1551    }
1552
1553    fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &AppContext) -> String {
1554        let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
1555            Some(worktree) => {
1556                let worktree = worktree.read(cx);
1557                match worktree.snapshot().root_entry() {
1558                    Some(root_entry) => {
1559                        if root_entry.id == entry.id {
1560                            file_name(worktree.abs_path().as_ref())
1561                        } else {
1562                            let path = worktree.absolutize(entry.path.as_ref()).ok();
1563                            let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
1564                            file_name(path)
1565                        }
1566                    }
1567                    None => {
1568                        let path = worktree.absolutize(entry.path.as_ref()).ok();
1569                        let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
1570                        file_name(path)
1571                    }
1572                }
1573            }
1574            None => file_name(entry.path.as_ref()),
1575        };
1576        name
1577    }
1578
1579    fn update_fs_entries(
1580        &mut self,
1581        active_editor: &View<Editor>,
1582        new_entries: HashSet<ExcerptId>,
1583        new_selected_entry: Option<EntryOwned>,
1584        debounce: Option<Duration>,
1585        cx: &mut ViewContext<Self>,
1586    ) {
1587        if !self.active {
1588            return;
1589        }
1590
1591        let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
1592        let active_multi_buffer = active_editor.read(cx).buffer().clone();
1593        let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
1594        let mut new_collapsed_entries = self.collapsed_entries.clone();
1595        let mut new_unfolded_dirs = self.unfolded_dirs.clone();
1596        let mut root_entries = HashSet::default();
1597        let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
1598        let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
1599            HashMap::default(),
1600            |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
1601                let buffer_id = buffer_snapshot.remote_id();
1602                let file = File::from_dyn(buffer_snapshot.file());
1603                let entry_id = file.and_then(|file| file.project_entry_id(cx));
1604                let worktree = file.map(|file| file.worktree.read(cx).snapshot());
1605                let is_new =
1606                    new_entries.contains(&excerpt_id) || !self.excerpts.contains_key(&buffer_id);
1607                buffer_excerpts
1608                    .entry(buffer_id)
1609                    .or_insert_with(|| (is_new, Vec::new(), entry_id, worktree))
1610                    .1
1611                    .push(excerpt_id);
1612
1613                let outlines = match self
1614                    .excerpts
1615                    .get(&buffer_id)
1616                    .and_then(|excerpts| excerpts.get(&excerpt_id))
1617                {
1618                    Some(old_excerpt) => match &old_excerpt.outlines {
1619                        ExcerptOutlines::Outlines(outlines) => {
1620                            ExcerptOutlines::Outlines(outlines.clone())
1621                        }
1622                        ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
1623                        ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
1624                    },
1625                    None => {
1626                        new_collapsed_entries
1627                            .insert(CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1628                        ExcerptOutlines::NotFetched
1629                    }
1630                };
1631                new_excerpts.entry(buffer_id).or_default().insert(
1632                    excerpt_id,
1633                    Excerpt {
1634                        range: excerpt_range,
1635                        outlines,
1636                    },
1637                );
1638                buffer_excerpts
1639            },
1640        );
1641
1642        self.updating_fs_entries = true;
1643        self.fs_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
1644            if let Some(debounce) = debounce {
1645                cx.background_executor().timer(debounce).await;
1646            }
1647            let Some((
1648                new_collapsed_entries,
1649                new_unfolded_dirs,
1650                new_fs_entries,
1651                new_depth_map,
1652                new_children_count,
1653            )) = cx
1654                .background_executor()
1655                .spawn(async move {
1656                    let mut processed_external_buffers = HashSet::default();
1657                    let mut new_worktree_entries =
1658                        HashMap::<WorktreeId, (worktree::Snapshot, HashSet<Entry>)>::default();
1659                    let mut worktree_excerpts = HashMap::<
1660                        WorktreeId,
1661                        HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
1662                    >::default();
1663                    let mut external_excerpts = HashMap::default();
1664
1665                    for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
1666                        if is_new {
1667                            match &worktree {
1668                                Some(worktree) => {
1669                                    new_collapsed_entries
1670                                        .insert(CollapsedEntry::File(worktree.id(), buffer_id));
1671                                }
1672                                None => {
1673                                    new_collapsed_entries
1674                                        .insert(CollapsedEntry::ExternalFile(buffer_id));
1675                                }
1676                            }
1677
1678                            for excerpt_id in &excerpts {
1679                                new_collapsed_entries
1680                                    .insert(CollapsedEntry::Excerpt(buffer_id, *excerpt_id));
1681                            }
1682                        }
1683
1684                        if let Some(worktree) = worktree {
1685                            let worktree_id = worktree.id();
1686                            let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
1687
1688                            match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
1689                                Some(entry) => {
1690                                    let mut traversal = worktree.traverse_from_path(
1691                                        true,
1692                                        true,
1693                                        true,
1694                                        entry.path.as_ref(),
1695                                    );
1696
1697                                    let mut entries_to_add = HashSet::default();
1698                                    worktree_excerpts
1699                                        .entry(worktree_id)
1700                                        .or_default()
1701                                        .insert(entry.id, (buffer_id, excerpts));
1702                                    let mut current_entry = entry;
1703                                    loop {
1704                                        if current_entry.is_dir() {
1705                                            let is_root =
1706                                                worktree.root_entry().map(|entry| entry.id)
1707                                                    == Some(current_entry.id);
1708                                            if is_root {
1709                                                root_entries.insert(current_entry.id);
1710                                                if auto_fold_dirs {
1711                                                    unfolded_dirs.insert(current_entry.id);
1712                                                }
1713                                            }
1714                                            if is_new {
1715                                                new_collapsed_entries.remove(&CollapsedEntry::Dir(
1716                                                    worktree_id,
1717                                                    current_entry.id,
1718                                                ));
1719                                            }
1720                                        }
1721
1722                                        let new_entry_added = entries_to_add.insert(current_entry);
1723                                        if new_entry_added && traversal.back_to_parent() {
1724                                            if let Some(parent_entry) = traversal.entry() {
1725                                                current_entry = parent_entry.clone();
1726                                                continue;
1727                                            }
1728                                        }
1729                                        break;
1730                                    }
1731                                    new_worktree_entries
1732                                        .entry(worktree_id)
1733                                        .or_insert_with(|| (worktree.clone(), HashSet::default()))
1734                                        .1
1735                                        .extend(entries_to_add);
1736                                }
1737                                None => {
1738                                    if processed_external_buffers.insert(buffer_id) {
1739                                        external_excerpts
1740                                            .entry(buffer_id)
1741                                            .or_insert_with(|| Vec::new())
1742                                            .extend(excerpts);
1743                                    }
1744                                }
1745                            }
1746                        } else if processed_external_buffers.insert(buffer_id) {
1747                            external_excerpts
1748                                .entry(buffer_id)
1749                                .or_insert_with(|| Vec::new())
1750                                .extend(excerpts);
1751                        }
1752                    }
1753
1754                    let mut new_children_count =
1755                        HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
1756
1757                    let worktree_entries = new_worktree_entries
1758                        .into_iter()
1759                        .map(|(worktree_id, (worktree_snapshot, entries))| {
1760                            let mut entries = entries.into_iter().collect::<Vec<_>>();
1761                            // For a proper git status propagation, we have to keep the entries sorted lexicographically.
1762                            entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
1763                            worktree_snapshot.propagate_git_statuses(&mut entries);
1764                            project::sort_worktree_entries(&mut entries);
1765                            (worktree_id, entries)
1766                        })
1767                        .flat_map(|(worktree_id, entries)| {
1768                            {
1769                                entries
1770                                    .into_iter()
1771                                    .filter_map(|entry| {
1772                                        if auto_fold_dirs {
1773                                            if let Some(parent) = entry.path.parent() {
1774                                                let children = new_children_count
1775                                                    .entry(worktree_id)
1776                                                    .or_default()
1777                                                    .entry(Arc::from(parent))
1778                                                    .or_default();
1779                                                if entry.is_dir() {
1780                                                    children.dirs += 1;
1781                                                } else {
1782                                                    children.files += 1;
1783                                                }
1784                                            }
1785                                        }
1786
1787                                        if entry.is_dir() {
1788                                            Some(FsEntry::Directory(worktree_id, entry))
1789                                        } else {
1790                                            let (buffer_id, excerpts) = worktree_excerpts
1791                                                .get_mut(&worktree_id)
1792                                                .and_then(|worktree_excerpts| {
1793                                                    worktree_excerpts.remove(&entry.id)
1794                                                })?;
1795                                            Some(FsEntry::File(
1796                                                worktree_id,
1797                                                entry,
1798                                                buffer_id,
1799                                                excerpts,
1800                                            ))
1801                                        }
1802                                    })
1803                                    .collect::<Vec<_>>()
1804                            }
1805                        })
1806                        .collect::<Vec<_>>();
1807
1808                    let mut visited_dirs = Vec::new();
1809                    let mut new_depth_map = HashMap::default();
1810                    let new_visible_entries = external_excerpts
1811                        .into_iter()
1812                        .sorted_by_key(|(id, _)| *id)
1813                        .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts))
1814                        .chain(worktree_entries)
1815                        .filter(|visible_item| {
1816                            match visible_item {
1817                                FsEntry::Directory(worktree_id, dir_entry) => {
1818                                    let parent_id = back_to_common_visited_parent(
1819                                        &mut visited_dirs,
1820                                        worktree_id,
1821                                        dir_entry,
1822                                    );
1823
1824                                    let depth = if root_entries.contains(&dir_entry.id) {
1825                                        0
1826                                    } else {
1827                                        if auto_fold_dirs {
1828                                            let children = new_children_count
1829                                                .get(&worktree_id)
1830                                                .and_then(|children_count| {
1831                                                    children_count.get(&dir_entry.path)
1832                                                })
1833                                                .copied()
1834                                                .unwrap_or_default();
1835
1836                                            if !children.may_be_fold_part()
1837                                                || (children.dirs == 0
1838                                                    && visited_dirs
1839                                                        .last()
1840                                                        .map(|(parent_dir_id, _)| {
1841                                                            new_unfolded_dirs
1842                                                                .get(&worktree_id)
1843                                                                .map_or(true, |unfolded_dirs| {
1844                                                                    unfolded_dirs
1845                                                                        .contains(&parent_dir_id)
1846                                                                })
1847                                                        })
1848                                                        .unwrap_or(true))
1849                                            {
1850                                                new_unfolded_dirs
1851                                                    .entry(*worktree_id)
1852                                                    .or_default()
1853                                                    .insert(dir_entry.id);
1854                                            }
1855                                        }
1856
1857                                        parent_id
1858                                            .and_then(|(worktree_id, id)| {
1859                                                new_depth_map.get(&(worktree_id, id)).copied()
1860                                            })
1861                                            .unwrap_or(0)
1862                                            + 1
1863                                    };
1864                                    visited_dirs.push((dir_entry.id, dir_entry.path.clone()));
1865                                    new_depth_map.insert((*worktree_id, dir_entry.id), depth);
1866                                }
1867                                FsEntry::File(worktree_id, file_entry, ..) => {
1868                                    let parent_id = back_to_common_visited_parent(
1869                                        &mut visited_dirs,
1870                                        worktree_id,
1871                                        file_entry,
1872                                    );
1873                                    let depth = if root_entries.contains(&file_entry.id) {
1874                                        0
1875                                    } else {
1876                                        parent_id
1877                                            .and_then(|(worktree_id, id)| {
1878                                                new_depth_map.get(&(worktree_id, id)).copied()
1879                                            })
1880                                            .unwrap_or(0)
1881                                            + 1
1882                                    };
1883                                    new_depth_map.insert((*worktree_id, file_entry.id), depth);
1884                                }
1885                                FsEntry::ExternalFile(..) => {
1886                                    visited_dirs.clear();
1887                                }
1888                            }
1889
1890                            true
1891                        })
1892                        .collect::<Vec<_>>();
1893
1894                    anyhow::Ok((
1895                        new_collapsed_entries,
1896                        new_unfolded_dirs,
1897                        new_visible_entries,
1898                        new_depth_map,
1899                        new_children_count,
1900                    ))
1901                })
1902                .await
1903                .log_err()
1904            else {
1905                return;
1906            };
1907
1908            outline_panel
1909                .update(&mut cx, |outline_panel, cx| {
1910                    outline_panel.updating_fs_entries = false;
1911                    outline_panel.excerpts = new_excerpts;
1912                    outline_panel.collapsed_entries = new_collapsed_entries;
1913                    outline_panel.unfolded_dirs = new_unfolded_dirs;
1914                    outline_panel.fs_entries = new_fs_entries;
1915                    outline_panel.fs_entries_depth = new_depth_map;
1916                    outline_panel.fs_children_count = new_children_count;
1917                    outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
1918                    if new_selected_entry.is_some() {
1919                        outline_panel.selected_entry = new_selected_entry;
1920                    }
1921                    outline_panel.fetch_outdated_outlines(cx);
1922                    outline_panel.autoscroll(cx);
1923                    cx.notify();
1924                })
1925                .ok();
1926        });
1927    }
1928
1929    fn replace_visible_entries(
1930        &mut self,
1931        new_active_editor: View<Editor>,
1932        cx: &mut ViewContext<Self>,
1933    ) {
1934        let new_selected_entry = self.location_for_editor_selection(&new_active_editor, cx);
1935        self.clear_previous(cx);
1936        self.active_item = Some(ActiveItem {
1937            item_id: new_active_editor.item_id(),
1938            _editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, cx),
1939            active_editor: new_active_editor.downgrade(),
1940        });
1941        let new_entries =
1942            HashSet::from_iter(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
1943        self.update_fs_entries(
1944            &new_active_editor,
1945            new_entries,
1946            new_selected_entry,
1947            None,
1948            cx,
1949        );
1950    }
1951
1952    fn clear_previous(&mut self, cx: &mut WindowContext<'_>) {
1953        self.filter_editor.update(cx, |editor, cx| editor.clear(cx));
1954        self.collapsed_entries.clear();
1955        self.unfolded_dirs.clear();
1956        self.selected_entry = None;
1957        self.fs_entries_update_task = Task::ready(());
1958        self.cached_entries_update_task = Task::ready(());
1959        self.active_item = None;
1960        self.fs_entries.clear();
1961        self.fs_entries_depth.clear();
1962        self.fs_children_count.clear();
1963        self.outline_fetch_tasks.clear();
1964        self.excerpts.clear();
1965        self.cached_entries_with_depth = Vec::new();
1966    }
1967
1968    fn location_for_editor_selection(
1969        &mut self,
1970        editor: &View<Editor>,
1971        cx: &mut ViewContext<Self>,
1972    ) -> Option<EntryOwned> {
1973        let selection = editor
1974            .read(cx)
1975            .selections
1976            .newest::<language::Point>(cx)
1977            .head();
1978        let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx));
1979        let multi_buffer = editor.read(cx).buffer();
1980        let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
1981        let (excerpt_id, buffer, _) = editor
1982            .read(cx)
1983            .buffer()
1984            .read(cx)
1985            .excerpt_containing(selection, cx)?;
1986        let buffer_id = buffer.read(cx).remote_id();
1987        let selection_display_point = selection.to_display_point(&editor_snapshot);
1988
1989        let excerpt_outlines = self
1990            .excerpts
1991            .get(&buffer_id)
1992            .and_then(|excerpts| excerpts.get(&excerpt_id))
1993            .into_iter()
1994            .flat_map(|excerpt| excerpt.iter_outlines())
1995            .flat_map(|outline| {
1996                let start = multi_buffer_snapshot
1997                    .anchor_in_excerpt(excerpt_id, outline.range.start)?
1998                    .to_display_point(&editor_snapshot);
1999                let end = multi_buffer_snapshot
2000                    .anchor_in_excerpt(excerpt_id, outline.range.end)?
2001                    .to_display_point(&editor_snapshot);
2002                Some((start..end, outline))
2003            })
2004            .collect::<Vec<_>>();
2005
2006        let mut matching_outline_indices = Vec::new();
2007        let mut children = HashMap::default();
2008        let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
2009
2010        for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
2011            if outline_range
2012                .to_inclusive()
2013                .contains(&selection_display_point)
2014            {
2015                matching_outline_indices.push(i);
2016            } else if (outline_range.start.row()..outline_range.end.row())
2017                .to_inclusive()
2018                .contains(&selection_display_point.row())
2019            {
2020                matching_outline_indices.push(i);
2021            }
2022
2023            while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
2024                if parent_outline.depth >= outline.depth
2025                    || !parent_range.contains(&outline_range.start)
2026                {
2027                    parents_stack.pop();
2028                } else {
2029                    break;
2030                }
2031            }
2032            if let Some((_, _, parent_index)) = parents_stack.last_mut() {
2033                children
2034                    .entry(*parent_index)
2035                    .or_insert_with(Vec::new)
2036                    .push(i);
2037            }
2038            parents_stack.push((outline_range, outline, i));
2039        }
2040
2041        let outline_item = matching_outline_indices
2042            .into_iter()
2043            .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
2044            .filter(|(i, _)| {
2045                children
2046                    .get(i)
2047                    .map(|children| {
2048                        children.iter().all(|child_index| {
2049                            excerpt_outlines
2050                                .get(*child_index)
2051                                .map(|(child_range, _)| child_range.start > selection_display_point)
2052                                .unwrap_or(false)
2053                        })
2054                    })
2055                    .unwrap_or(true)
2056            })
2057            .min_by_key(|(_, (outline_range, outline))| {
2058                let distance_from_start = if outline_range.start > selection_display_point {
2059                    outline_range.start - selection_display_point
2060                } else {
2061                    selection_display_point - outline_range.start
2062                };
2063                let distance_from_end = if outline_range.end > selection_display_point {
2064                    outline_range.end - selection_display_point
2065                } else {
2066                    selection_display_point - outline_range.end
2067                };
2068
2069                (
2070                    cmp::Reverse(outline.depth),
2071                    distance_from_start + distance_from_end,
2072                )
2073            })
2074            .map(|(_, (_, outline))| *outline)
2075            .cloned();
2076
2077        let closest_container = match outline_item {
2078            Some(outline) => EntryOwned::Outline(buffer_id, excerpt_id, outline),
2079            None => self
2080                .cached_entries_with_depth
2081                .iter()
2082                .rev()
2083                .find_map(|cached_entry| match &cached_entry.entry {
2084                    EntryOwned::Excerpt(entry_buffer_id, entry_excerpt_id, _) => {
2085                        if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id {
2086                            Some(cached_entry.entry.clone())
2087                        } else {
2088                            None
2089                        }
2090                    }
2091                    EntryOwned::Entry(
2092                        FsEntry::ExternalFile(file_buffer_id, file_excerpts)
2093                        | FsEntry::File(_, _, file_buffer_id, file_excerpts),
2094                    ) => {
2095                        if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
2096                            Some(cached_entry.entry.clone())
2097                        } else {
2098                            None
2099                        }
2100                    }
2101                    _ => None,
2102                })?,
2103        };
2104        Some(closest_container)
2105    }
2106
2107    fn fetch_outdated_outlines(&mut self, cx: &mut ViewContext<Self>) {
2108        let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
2109        if excerpt_fetch_ranges.is_empty() {
2110            return;
2111        }
2112
2113        let syntax_theme = cx.theme().syntax().clone();
2114        for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
2115            for (excerpt_id, excerpt_range) in excerpt_ranges {
2116                let syntax_theme = syntax_theme.clone();
2117                let buffer_snapshot = buffer_snapshot.clone();
2118                self.outline_fetch_tasks.insert(
2119                    (buffer_id, excerpt_id),
2120                    cx.spawn(|outline_panel, mut cx| async move {
2121                        let fetched_outlines = cx
2122                            .background_executor()
2123                            .spawn(async move {
2124                                buffer_snapshot
2125                                    .outline_items_containing(
2126                                        excerpt_range.context,
2127                                        false,
2128                                        Some(&syntax_theme),
2129                                    )
2130                                    .unwrap_or_default()
2131                            })
2132                            .await;
2133                        outline_panel
2134                            .update(&mut cx, |outline_panel, cx| {
2135                                if let Some(excerpt) = outline_panel
2136                                    .excerpts
2137                                    .entry(buffer_id)
2138                                    .or_default()
2139                                    .get_mut(&excerpt_id)
2140                                {
2141                                    excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
2142                                }
2143                                outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
2144                            })
2145                            .ok();
2146                    }),
2147                );
2148            }
2149        }
2150    }
2151
2152    fn is_singleton_active(&self, cx: &AppContext) -> bool {
2153        self.active_item
2154            .as_ref()
2155            .and_then(|active_item| {
2156                Some(
2157                    active_item
2158                        .active_editor
2159                        .upgrade()?
2160                        .read(cx)
2161                        .buffer()
2162                        .read(cx)
2163                        .is_singleton(),
2164                )
2165            })
2166            .unwrap_or(false)
2167    }
2168
2169    fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
2170        self.outline_fetch_tasks.clear();
2171        let mut ids = ids.into_iter().collect::<HashSet<_>>();
2172        for excerpts in self.excerpts.values_mut() {
2173            ids.retain(|id| {
2174                if let Some(excerpt) = excerpts.get_mut(id) {
2175                    excerpt.invalidate_outlines();
2176                    false
2177                } else {
2178                    true
2179                }
2180            });
2181            if ids.is_empty() {
2182                break;
2183            }
2184        }
2185    }
2186
2187    fn excerpt_fetch_ranges(
2188        &self,
2189        cx: &AppContext,
2190    ) -> HashMap<
2191        BufferId,
2192        (
2193            BufferSnapshot,
2194            HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
2195        ),
2196    > {
2197        self.fs_entries
2198            .iter()
2199            .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
2200                match fs_entry {
2201                    FsEntry::File(_, _, buffer_id, file_excerpts)
2202                    | FsEntry::ExternalFile(buffer_id, file_excerpts) => {
2203                        let excerpts = self.excerpts.get(&buffer_id);
2204                        for &file_excerpt in file_excerpts {
2205                            if let Some(excerpt) = excerpts
2206                                .and_then(|excerpts| excerpts.get(&file_excerpt))
2207                                .filter(|excerpt| excerpt.should_fetch_outlines())
2208                            {
2209                                match excerpts_to_fetch.entry(*buffer_id) {
2210                                    hash_map::Entry::Occupied(mut o) => {
2211                                        o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
2212                                    }
2213                                    hash_map::Entry::Vacant(v) => {
2214                                        if let Some(buffer_snapshot) =
2215                                            self.buffer_snapshot_for_id(*buffer_id, cx)
2216                                        {
2217                                            v.insert((buffer_snapshot, HashMap::default()))
2218                                                .1
2219                                                .insert(file_excerpt, excerpt.range.clone());
2220                                        }
2221                                    }
2222                                }
2223                            }
2224                        }
2225                    }
2226                    FsEntry::Directory(..) => {}
2227                }
2228                excerpts_to_fetch
2229            })
2230    }
2231
2232    fn buffer_snapshot_for_id(
2233        &self,
2234        buffer_id: BufferId,
2235        cx: &AppContext,
2236    ) -> Option<BufferSnapshot> {
2237        let editor = self.active_item.as_ref()?.active_editor.upgrade()?;
2238        Some(
2239            editor
2240                .read(cx)
2241                .buffer()
2242                .read(cx)
2243                .buffer(buffer_id)?
2244                .read(cx)
2245                .snapshot(),
2246        )
2247    }
2248
2249    fn abs_path(&self, entry: &EntryOwned, cx: &AppContext) -> Option<PathBuf> {
2250        match entry {
2251            EntryOwned::Entry(
2252                FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _),
2253            ) => self
2254                .buffer_snapshot_for_id(*buffer_id, cx)
2255                .and_then(|buffer_snapshot| {
2256                    let file = File::from_dyn(buffer_snapshot.file())?;
2257                    file.worktree.read(cx).absolutize(&file.path).ok()
2258                }),
2259            EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => self
2260                .project
2261                .read(cx)
2262                .worktree_for_id(*worktree_id, cx)?
2263                .read(cx)
2264                .absolutize(&entry.path)
2265                .ok(),
2266            EntryOwned::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
2267                self.project
2268                    .read(cx)
2269                    .worktree_for_id(*worktree_id, cx)
2270                    .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
2271            }),
2272            EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
2273        }
2274    }
2275
2276    fn relative_path(&self, entry: &FsEntry, cx: &AppContext) -> Option<Arc<Path>> {
2277        match entry {
2278            FsEntry::ExternalFile(buffer_id, _) => {
2279                let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
2280                Some(buffer_snapshot.file()?.path().clone())
2281            }
2282            FsEntry::Directory(_, entry) => Some(entry.path.clone()),
2283            FsEntry::File(_, entry, ..) => Some(entry.path.clone()),
2284        }
2285    }
2286
2287    fn update_cached_entries(
2288        &mut self,
2289        debounce: Option<Duration>,
2290        cx: &mut ViewContext<OutlinePanel>,
2291    ) {
2292        let is_singleton = self.is_singleton_active(cx);
2293        let query = self.query(cx);
2294        self.cached_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
2295            if let Some(debounce) = debounce {
2296                cx.background_executor().timer(debounce).await;
2297            }
2298            let Some(new_cached_entries) = outline_panel
2299                .update(&mut cx, |outline_panel, cx| {
2300                    outline_panel.generate_cached_entries(is_singleton, query, cx)
2301                })
2302                .ok()
2303            else {
2304                return;
2305            };
2306            let new_cached_entries = new_cached_entries.await;
2307            outline_panel
2308                .update(&mut cx, |outline_panel, cx| {
2309                    outline_panel.cached_entries_with_depth = new_cached_entries;
2310                    cx.notify();
2311                })
2312                .ok();
2313        });
2314    }
2315
2316    fn generate_cached_entries(
2317        &self,
2318        is_singleton: bool,
2319        query: Option<String>,
2320        cx: &mut ViewContext<'_, Self>,
2321    ) -> Task<Vec<CachedEntry>> {
2322        let project = self.project.clone();
2323        cx.spawn(|outline_panel, mut cx| async move {
2324            let mut entries = Vec::new();
2325            let mut match_candidates = Vec::new();
2326
2327            let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| {
2328                let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2329                let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec<Entry>)>;
2330                let track_matches = query.is_some();
2331                let mut parent_dirs = Vec::<(&Path, bool, bool, usize)>::new();
2332
2333                for entry in &outline_panel.fs_entries {
2334                    let is_expanded = outline_panel.is_expanded(entry);
2335                    let (depth, should_add) = match entry {
2336                        FsEntry::Directory(worktree_id, dir_entry) => {
2337                            let is_root = project
2338                                .read(cx)
2339                                .worktree_for_id(*worktree_id, cx)
2340                                .map_or(false, |worktree| {
2341                                    worktree.read(cx).root_entry() == Some(dir_entry)
2342                                });
2343                            let folded = auto_fold_dirs
2344                                && !is_root
2345                                && outline_panel
2346                                    .unfolded_dirs
2347                                    .get(worktree_id)
2348                                    .map_or(true, |unfolded_dirs| {
2349                                        !unfolded_dirs.contains(&dir_entry.id)
2350                                    });
2351                            let fs_depth = outline_panel
2352                                .fs_entries_depth
2353                                .get(&(*worktree_id, dir_entry.id))
2354                                .copied()
2355                                .unwrap_or(0);
2356                            while let Some(&(previous_path, ..)) = parent_dirs.last() {
2357                                if dir_entry.path.starts_with(previous_path) {
2358                                    break;
2359                                }
2360                                parent_dirs.pop();
2361                            }
2362                            let auto_fold = match parent_dirs.last() {
2363                                Some((parent_path, parent_folded, _, _)) => {
2364                                    *parent_folded
2365                                        && Some(*parent_path) == dir_entry.path.parent()
2366                                        && outline_panel
2367                                            .fs_children_count
2368                                            .get(worktree_id)
2369                                            .and_then(|entries| entries.get(&dir_entry.path))
2370                                            .copied()
2371                                            .unwrap_or_default()
2372                                            .may_be_fold_part()
2373                                }
2374                                None => false,
2375                            };
2376                            let folded = folded || auto_fold;
2377                            let (depth, parent_expanded) = match parent_dirs.last() {
2378                                Some(&(_, previous_folded, previous_expanded, previous_depth)) => {
2379                                    let new_depth = if folded && previous_folded {
2380                                        previous_depth
2381                                    } else {
2382                                        previous_depth + 1
2383                                    };
2384                                    parent_dirs.push((
2385                                        &dir_entry.path,
2386                                        folded,
2387                                        previous_expanded && is_expanded,
2388                                        new_depth,
2389                                    ));
2390                                    (new_depth, previous_expanded)
2391                                }
2392                                None => {
2393                                    parent_dirs.push((
2394                                        &dir_entry.path,
2395                                        folded,
2396                                        is_expanded,
2397                                        fs_depth,
2398                                    ));
2399                                    (fs_depth, true)
2400                                }
2401                            };
2402
2403                            if let Some((folded_depth, folded_worktree_id, mut folded_dirs)) =
2404                                folded_dirs_entry.take()
2405                            {
2406                                if folded
2407                                    && worktree_id == &folded_worktree_id
2408                                    && dir_entry.path.parent()
2409                                        == folded_dirs.last().map(|entry| entry.path.as_ref())
2410                                {
2411                                    folded_dirs.push(dir_entry.clone());
2412                                    folded_dirs_entry =
2413                                        Some((folded_depth, folded_worktree_id, folded_dirs))
2414                                } else {
2415                                    if parent_expanded || query.is_some() {
2416                                        let new_folded_dirs =
2417                                            EntryOwned::FoldedDirs(folded_worktree_id, folded_dirs);
2418                                        outline_panel.push_entry(
2419                                            &mut entries,
2420                                            &mut match_candidates,
2421                                            track_matches,
2422                                            new_folded_dirs,
2423                                            folded_depth,
2424                                            cx,
2425                                        );
2426                                    }
2427                                    folded_dirs_entry =
2428                                        Some((depth, *worktree_id, vec![dir_entry.clone()]))
2429                                }
2430                            } else if folded {
2431                                folded_dirs_entry =
2432                                    Some((depth, *worktree_id, vec![dir_entry.clone()]));
2433                            }
2434
2435                            let should_add = parent_expanded && folded_dirs_entry.is_none();
2436                            (depth, should_add)
2437                        }
2438                        FsEntry::ExternalFile(..) => {
2439                            if let Some((folded_depth, worktree_id, folded_dirs)) =
2440                                folded_dirs_entry.take()
2441                            {
2442                                let parent_expanded = parent_dirs
2443                                    .iter()
2444                                    .rev()
2445                                    .find(|(parent_path, ..)| {
2446                                        folded_dirs
2447                                            .iter()
2448                                            .all(|entry| entry.path.as_ref() != *parent_path)
2449                                    })
2450                                    .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
2451                                if parent_expanded || query.is_some() {
2452                                    outline_panel.push_entry(
2453                                        &mut entries,
2454                                        &mut match_candidates,
2455                                        track_matches,
2456                                        EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2457                                        folded_depth,
2458                                        cx,
2459                                    );
2460                                }
2461                            }
2462                            parent_dirs.clear();
2463                            (0, true)
2464                        }
2465                        FsEntry::File(worktree_id, file_entry, ..) => {
2466                            if let Some((folded_depth, worktree_id, folded_dirs)) =
2467                                folded_dirs_entry.take()
2468                            {
2469                                let parent_expanded = parent_dirs
2470                                    .iter()
2471                                    .rev()
2472                                    .find(|(parent_path, ..)| {
2473                                        folded_dirs
2474                                            .iter()
2475                                            .all(|entry| entry.path.as_ref() != *parent_path)
2476                                    })
2477                                    .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
2478                                if parent_expanded || query.is_some() {
2479                                    outline_panel.push_entry(
2480                                        &mut entries,
2481                                        &mut match_candidates,
2482                                        track_matches,
2483                                        EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2484                                        folded_depth,
2485                                        cx,
2486                                    );
2487                                }
2488                            }
2489
2490                            let fs_depth = outline_panel
2491                                .fs_entries_depth
2492                                .get(&(*worktree_id, file_entry.id))
2493                                .copied()
2494                                .unwrap_or(0);
2495                            while let Some(&(previous_path, ..)) = parent_dirs.last() {
2496                                if file_entry.path.starts_with(previous_path) {
2497                                    break;
2498                                }
2499                                parent_dirs.pop();
2500                            }
2501                            let (depth, should_add) = match parent_dirs.last() {
2502                                Some(&(_, _, previous_expanded, previous_depth)) => {
2503                                    let new_depth = previous_depth + 1;
2504                                    (new_depth, previous_expanded)
2505                                }
2506                                None => (fs_depth, true),
2507                            };
2508                            (depth, should_add)
2509                        }
2510                    };
2511
2512                    if !is_singleton
2513                        && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
2514                    {
2515                        outline_panel.push_entry(
2516                            &mut entries,
2517                            &mut match_candidates,
2518                            track_matches,
2519                            EntryOwned::Entry(entry.clone()),
2520                            depth,
2521                            cx,
2522                        );
2523                    }
2524
2525                    let excerpts_to_consider =
2526                        if is_singleton || query.is_some() || (should_add && is_expanded) {
2527                            match entry {
2528                                FsEntry::File(_, _, buffer_id, entry_excerpts) => {
2529                                    Some((*buffer_id, entry_excerpts))
2530                                }
2531                                FsEntry::ExternalFile(buffer_id, entry_excerpts) => {
2532                                    Some((*buffer_id, entry_excerpts))
2533                                }
2534                                _ => None,
2535                            }
2536                        } else {
2537                            None
2538                        };
2539                    if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
2540                        if let Some(excerpts) = outline_panel.excerpts.get(&buffer_id) {
2541                            for &entry_excerpt in entry_excerpts {
2542                                let Some(excerpt) = excerpts.get(&entry_excerpt) else {
2543                                    continue;
2544                                };
2545                                let excerpt_depth = depth + 1;
2546                                outline_panel.push_entry(
2547                                    &mut entries,
2548                                    &mut match_candidates,
2549                                    track_matches,
2550                                    EntryOwned::Excerpt(
2551                                        buffer_id,
2552                                        entry_excerpt,
2553                                        excerpt.range.clone(),
2554                                    ),
2555                                    excerpt_depth,
2556                                    cx,
2557                                );
2558
2559                                let mut outline_base_depth = excerpt_depth + 1;
2560                                if is_singleton {
2561                                    outline_base_depth = 0;
2562                                    entries.clear();
2563                                    match_candidates.clear();
2564                                } else if query.is_none()
2565                                    && outline_panel.collapsed_entries.contains(
2566                                        &CollapsedEntry::Excerpt(buffer_id, entry_excerpt),
2567                                    )
2568                                {
2569                                    continue;
2570                                }
2571
2572                                for outline in excerpt.iter_outlines() {
2573                                    outline_panel.push_entry(
2574                                        &mut entries,
2575                                        &mut match_candidates,
2576                                        track_matches,
2577                                        EntryOwned::Outline(
2578                                            buffer_id,
2579                                            entry_excerpt,
2580                                            outline.clone(),
2581                                        ),
2582                                        outline_base_depth + outline.depth,
2583                                        cx,
2584                                    );
2585                                }
2586                                if is_singleton && entries.is_empty() {
2587                                    outline_panel.push_entry(
2588                                        &mut entries,
2589                                        &mut match_candidates,
2590                                        track_matches,
2591                                        EntryOwned::Entry(entry.clone()),
2592                                        0,
2593                                        cx,
2594                                    );
2595                                }
2596                            }
2597                        }
2598                    }
2599                }
2600
2601                if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
2602                    let parent_expanded = parent_dirs
2603                        .iter()
2604                        .rev()
2605                        .find(|(parent_path, ..)| {
2606                            folded_dirs
2607                                .iter()
2608                                .all(|entry| entry.path.as_ref() != *parent_path)
2609                        })
2610                        .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
2611                    if parent_expanded || query.is_some() {
2612                        outline_panel.push_entry(
2613                            &mut entries,
2614                            &mut match_candidates,
2615                            track_matches,
2616                            EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2617                            folded_depth,
2618                            cx,
2619                        );
2620                    }
2621                }
2622            }) else {
2623                return Vec::new();
2624            };
2625
2626            let Some(query) = query else {
2627                return entries;
2628            };
2629            let mut matched_ids = match_strings(
2630                &match_candidates,
2631                &query,
2632                true,
2633                usize::MAX,
2634                &AtomicBool::default(),
2635                cx.background_executor().clone(),
2636            )
2637            .await
2638            .into_iter()
2639            .map(|string_match| (string_match.candidate_id, string_match))
2640            .collect::<HashMap<_, _>>();
2641
2642            let mut id = 0;
2643            entries.retain_mut(|cached_entry| {
2644                let retain = match matched_ids.remove(&id) {
2645                    Some(string_match) => {
2646                        cached_entry.string_match = Some(string_match);
2647                        true
2648                    }
2649                    None => false,
2650                };
2651                id += 1;
2652                retain
2653            });
2654
2655            entries
2656        })
2657    }
2658
2659    fn push_entry(
2660        &self,
2661        entries: &mut Vec<CachedEntry>,
2662        match_candidates: &mut Vec<StringMatchCandidate>,
2663        track_matches: bool,
2664        entry: EntryOwned,
2665        depth: usize,
2666        cx: &AppContext,
2667    ) {
2668        if track_matches {
2669            let id = entries.len();
2670            match &entry {
2671                EntryOwned::Entry(fs_entry) => {
2672                    if let Some(file_name) =
2673                        self.relative_path(fs_entry, cx).as_deref().map(file_name)
2674                    {
2675                        match_candidates.push(StringMatchCandidate {
2676                            id,
2677                            string: file_name.to_string(),
2678                            char_bag: file_name.chars().collect(),
2679                        });
2680                    }
2681                }
2682                EntryOwned::FoldedDirs(worktree_id, entries) => {
2683                    let dir_names = self.dir_names_string(entries, *worktree_id, cx);
2684                    {
2685                        match_candidates.push(StringMatchCandidate {
2686                            id,
2687                            string: dir_names.to_string(),
2688                            char_bag: dir_names.chars().collect(),
2689                        });
2690                    }
2691                }
2692                EntryOwned::Outline(_, _, outline) => match_candidates.push(StringMatchCandidate {
2693                    id,
2694                    string: outline.text.clone(),
2695                    char_bag: outline.text.chars().collect(),
2696                }),
2697                EntryOwned::Excerpt(..) => {}
2698            }
2699        }
2700        entries.push(CachedEntry {
2701            depth,
2702            entry,
2703            string_match: None,
2704        });
2705    }
2706
2707    fn dir_names_string(
2708        &self,
2709        entries: &[Entry],
2710        worktree_id: WorktreeId,
2711        cx: &AppContext,
2712    ) -> String {
2713        let dir_names_segment = entries
2714            .iter()
2715            .map(|entry| self.entry_name(&worktree_id, entry, cx))
2716            .collect::<PathBuf>();
2717        dir_names_segment.to_string_lossy().to_string()
2718    }
2719
2720    fn query(&self, cx: &AppContext) -> Option<String> {
2721        let query = self.filter_editor.read(cx).text(cx);
2722        if query.trim().is_empty() {
2723            None
2724        } else {
2725            Some(query)
2726        }
2727    }
2728
2729    fn is_expanded(&self, entry: &FsEntry) -> bool {
2730        let entry_to_check = match entry {
2731            FsEntry::ExternalFile(buffer_id, _) => CollapsedEntry::ExternalFile(*buffer_id),
2732            FsEntry::File(worktree_id, _, buffer_id, _) => {
2733                CollapsedEntry::File(*worktree_id, *buffer_id)
2734            }
2735            FsEntry::Directory(worktree_id, entry) => CollapsedEntry::Dir(*worktree_id, entry.id),
2736        };
2737        !self.collapsed_entries.contains(&entry_to_check)
2738    }
2739}
2740
2741fn back_to_common_visited_parent(
2742    visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
2743    worktree_id: &WorktreeId,
2744    new_entry: &Entry,
2745) -> Option<(WorktreeId, ProjectEntryId)> {
2746    while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
2747        match new_entry.path.parent() {
2748            Some(parent_path) => {
2749                if parent_path == visited_path.as_ref() {
2750                    return Some((*worktree_id, *visited_dir_id));
2751                }
2752            }
2753            None => {
2754                break;
2755            }
2756        }
2757        visited_dirs.pop();
2758    }
2759    None
2760}
2761
2762fn file_name(path: &Path) -> String {
2763    let mut current_path = path;
2764    loop {
2765        if let Some(file_name) = current_path.file_name() {
2766            return file_name.to_string_lossy().into_owned();
2767        }
2768        match current_path.parent() {
2769            Some(parent) => current_path = parent,
2770            None => return path.to_string_lossy().into_owned(),
2771        }
2772    }
2773}
2774
2775impl Panel for OutlinePanel {
2776    fn persistent_name() -> &'static str {
2777        "Outline Panel"
2778    }
2779
2780    fn position(&self, cx: &WindowContext) -> DockPosition {
2781        match OutlinePanelSettings::get_global(cx).dock {
2782            OutlinePanelDockPosition::Left => DockPosition::Left,
2783            OutlinePanelDockPosition::Right => DockPosition::Right,
2784        }
2785    }
2786
2787    fn position_is_valid(&self, position: DockPosition) -> bool {
2788        matches!(position, DockPosition::Left | DockPosition::Right)
2789    }
2790
2791    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
2792        settings::update_settings_file::<OutlinePanelSettings>(
2793            self.fs.clone(),
2794            cx,
2795            move |settings| {
2796                let dock = match position {
2797                    DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
2798                    DockPosition::Right => OutlinePanelDockPosition::Right,
2799                };
2800                settings.dock = Some(dock);
2801            },
2802        );
2803    }
2804
2805    fn size(&self, cx: &WindowContext) -> Pixels {
2806        self.width
2807            .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
2808    }
2809
2810    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
2811        self.width = size;
2812        self.serialize(cx);
2813        cx.notify();
2814    }
2815
2816    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
2817        OutlinePanelSettings::get_global(cx)
2818            .button
2819            .then(|| IconName::ListTree)
2820    }
2821
2822    fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
2823        Some("Outline Panel")
2824    }
2825
2826    fn toggle_action(&self) -> Box<dyn Action> {
2827        Box::new(ToggleFocus)
2828    }
2829
2830    fn starts_open(&self, _: &WindowContext) -> bool {
2831        self.active
2832    }
2833
2834    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2835        let old_active = self.active;
2836        self.active = active;
2837        if active && old_active != active {
2838            if let Some(active_editor) = self
2839                .active_item
2840                .as_ref()
2841                .and_then(|item| item.active_editor.upgrade())
2842            {
2843                if self.active_item.as_ref().map(|item| item.item_id)
2844                    == Some(active_editor.item_id())
2845                {
2846                    let new_selected_entry = self.location_for_editor_selection(&active_editor, cx);
2847                    self.update_fs_entries(
2848                        &active_editor,
2849                        HashSet::default(),
2850                        new_selected_entry,
2851                        None,
2852                        cx,
2853                    )
2854                } else {
2855                    self.replace_visible_entries(active_editor, cx);
2856                }
2857            }
2858        }
2859        self.serialize(cx);
2860    }
2861}
2862
2863impl FocusableView for OutlinePanel {
2864    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2865        self.filter_editor.focus_handle(cx).clone()
2866    }
2867}
2868
2869impl EventEmitter<Event> for OutlinePanel {}
2870
2871impl EventEmitter<PanelEvent> for OutlinePanel {}
2872
2873impl Render for OutlinePanel {
2874    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2875        let project = self.project.read(cx);
2876        let query = self.query(cx);
2877        let outline_panel = v_flex()
2878            .id("outline-panel")
2879            .size_full()
2880            .relative()
2881            .key_context(self.dispatch_context(cx))
2882            .on_action(cx.listener(Self::open))
2883            .on_action(cx.listener(Self::cancel))
2884            .on_action(cx.listener(Self::select_next))
2885            .on_action(cx.listener(Self::select_prev))
2886            .on_action(cx.listener(Self::select_first))
2887            .on_action(cx.listener(Self::select_last))
2888            .on_action(cx.listener(Self::select_parent))
2889            .on_action(cx.listener(Self::expand_selected_entry))
2890            .on_action(cx.listener(Self::collapse_selected_entry))
2891            .on_action(cx.listener(Self::expand_all_entries))
2892            .on_action(cx.listener(Self::collapse_all_entries))
2893            .on_action(cx.listener(Self::copy_path))
2894            .on_action(cx.listener(Self::copy_relative_path))
2895            .on_action(cx.listener(Self::unfold_directory))
2896            .on_action(cx.listener(Self::fold_directory))
2897            .when(project.is_local(), |el| {
2898                el.on_action(cx.listener(Self::reveal_in_finder))
2899                    .on_action(cx.listener(Self::open_in_terminal))
2900            })
2901            .on_mouse_down(
2902                MouseButton::Right,
2903                cx.listener(move |outline_panel, event: &MouseDownEvent, cx| {
2904                    if let Some(entry) = outline_panel.selected_entry.clone() {
2905                        outline_panel.deploy_context_menu(event.position, entry.to_ref_entry(), cx)
2906                    } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
2907                        outline_panel.deploy_context_menu(
2908                            event.position,
2909                            EntryRef::Entry(&entry),
2910                            cx,
2911                        )
2912                    }
2913                }),
2914            )
2915            .track_focus(&self.focus_handle);
2916
2917        if self.cached_entries_with_depth.is_empty() {
2918            let header = if self.updating_fs_entries {
2919                "Loading outlines"
2920            } else if query.is_some() {
2921                "No matches for query"
2922            } else {
2923                "No outlines available"
2924            };
2925
2926            outline_panel.child(
2927                v_flex()
2928                    .justify_center()
2929                    .size_full()
2930                    .child(h_flex().justify_center().child(Label::new(header)))
2931                    .when_some(query.clone(), |panel, query| {
2932                        panel.child(h_flex().justify_center().child(Label::new(query)))
2933                    })
2934                    .child(
2935                        h_flex()
2936                            .pt(Spacing::Small.rems(cx))
2937                            .justify_center()
2938                            .child({
2939                                let keystroke = match self.position(cx) {
2940                                    DockPosition::Left => {
2941                                        cx.keystroke_text_for(&workspace::ToggleLeftDock)
2942                                    }
2943                                    DockPosition::Bottom => {
2944                                        cx.keystroke_text_for(&workspace::ToggleBottomDock)
2945                                    }
2946                                    DockPosition::Right => {
2947                                        cx.keystroke_text_for(&workspace::ToggleRightDock)
2948                                    }
2949                                };
2950                                Label::new(format!("Toggle this panel with {keystroke}"))
2951                            }),
2952                    ),
2953            )
2954        } else {
2955            outline_panel.child({
2956                let items_len = self.cached_entries_with_depth.len();
2957                uniform_list(cx.view().clone(), "entries", items_len, {
2958                    move |outline_panel, range, cx| {
2959                        let entries = outline_panel.cached_entries_with_depth.get(range);
2960                        entries
2961                            .map(|entries| entries.to_vec())
2962                            .unwrap_or_default()
2963                            .into_iter()
2964                            .filter_map(|cached_entry| match cached_entry.entry {
2965                                EntryOwned::Entry(entry) => Some(outline_panel.render_entry(
2966                                    &entry,
2967                                    cached_entry.depth,
2968                                    cached_entry.string_match.as_ref(),
2969                                    cx,
2970                                )),
2971                                EntryOwned::FoldedDirs(worktree_id, entries) => {
2972                                    Some(outline_panel.render_folded_dirs(
2973                                        worktree_id,
2974                                        &entries,
2975                                        cached_entry.depth,
2976                                        cached_entry.string_match.as_ref(),
2977                                        cx,
2978                                    ))
2979                                }
2980                                EntryOwned::Excerpt(buffer_id, excerpt_id, excerpt) => {
2981                                    outline_panel.render_excerpt(
2982                                        buffer_id,
2983                                        excerpt_id,
2984                                        &excerpt,
2985                                        cached_entry.depth,
2986                                        cx,
2987                                    )
2988                                }
2989                                EntryOwned::Outline(buffer_id, excerpt_id, outline) => {
2990                                    Some(outline_panel.render_outline(
2991                                        buffer_id,
2992                                        excerpt_id,
2993                                        &outline,
2994                                        cached_entry.depth,
2995                                        cached_entry.string_match.as_ref(),
2996                                        cx,
2997                                    ))
2998                                }
2999                            })
3000                            .collect()
3001                    }
3002                })
3003                .size_full()
3004                .track_scroll(self.scroll_handle.clone())
3005            })
3006        }
3007        .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3008            deferred(
3009                anchored()
3010                    .position(*position)
3011                    .anchor(gpui::AnchorCorner::TopLeft)
3012                    .child(menu.clone()),
3013            )
3014            .with_priority(1)
3015        }))
3016        .child(
3017            v_flex()
3018                .child(div().mx_2().border_primary(cx).border_t_1())
3019                .child(v_flex().p_2().child(self.filter_editor.clone())),
3020        )
3021    }
3022}
3023
3024fn subscribe_for_editor_events(
3025    editor: &View<Editor>,
3026    cx: &mut ViewContext<OutlinePanel>,
3027) -> Subscription {
3028    let debounce = Some(UPDATE_DEBOUNCE);
3029    cx.subscribe(
3030        editor,
3031        move |outline_panel, editor, e: &EditorEvent, cx| match e {
3032            EditorEvent::SelectionsChanged { local: true } => {
3033                outline_panel.reveal_entry_for_selection(&editor, cx);
3034                cx.notify();
3035            }
3036            EditorEvent::ExcerptsAdded { excerpts, .. } => {
3037                outline_panel.update_fs_entries(
3038                    &editor,
3039                    excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(),
3040                    None,
3041                    debounce,
3042                    cx,
3043                );
3044            }
3045            EditorEvent::ExcerptsRemoved { ids } => {
3046                let mut ids = ids.into_iter().collect::<HashSet<_>>();
3047                for excerpts in outline_panel.excerpts.values_mut() {
3048                    excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
3049                    if ids.is_empty() {
3050                        break;
3051                    }
3052                }
3053                outline_panel.update_fs_entries(&editor, HashSet::default(), None, debounce, cx);
3054            }
3055            EditorEvent::ExcerptsExpanded { ids } => {
3056                outline_panel.invalidate_outlines(ids);
3057                outline_panel.fetch_outdated_outlines(cx)
3058            }
3059            EditorEvent::ExcerptsEdited { ids } => {
3060                outline_panel.invalidate_outlines(ids);
3061                outline_panel.fetch_outdated_outlines(cx);
3062            }
3063            EditorEvent::Reparsed(buffer_id) => {
3064                if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
3065                    for (_, excerpt) in excerpts {
3066                        excerpt.invalidate_outlines();
3067                    }
3068                }
3069                outline_panel.fetch_outdated_outlines(cx);
3070            }
3071            _ => {}
3072        },
3073    )
3074}
3075
3076fn empty_icon() -> AnyElement {
3077    h_flex()
3078        .size(IconSize::default().rems())
3079        .invisible()
3080        .flex_none()
3081        .into_any_element()
3082}