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, cx)
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, cx)
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 => ExcerptOutlines::NotFetched,
1626                };
1627                new_excerpts.entry(buffer_id).or_default().insert(
1628                    excerpt_id,
1629                    Excerpt {
1630                        range: excerpt_range,
1631                        outlines,
1632                    },
1633                );
1634                buffer_excerpts
1635            },
1636        );
1637
1638        self.updating_fs_entries = true;
1639        self.fs_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
1640            if let Some(debounce) = debounce {
1641                cx.background_executor().timer(debounce).await;
1642            }
1643            let Some((
1644                new_collapsed_entries,
1645                new_unfolded_dirs,
1646                new_fs_entries,
1647                new_depth_map,
1648                new_children_count,
1649            )) = cx
1650                .background_executor()
1651                .spawn(async move {
1652                    let mut processed_external_buffers = HashSet::default();
1653                    let mut new_worktree_entries =
1654                        HashMap::<WorktreeId, (worktree::Snapshot, HashSet<Entry>)>::default();
1655                    let mut worktree_excerpts = HashMap::<
1656                        WorktreeId,
1657                        HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
1658                    >::default();
1659                    let mut external_excerpts = HashMap::default();
1660
1661                    for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
1662                        if is_new {
1663                            match &worktree {
1664                                Some(worktree) => {
1665                                    new_collapsed_entries
1666                                        .insert(CollapsedEntry::File(worktree.id(), buffer_id));
1667                                }
1668                                None => {
1669                                    new_collapsed_entries
1670                                        .insert(CollapsedEntry::ExternalFile(buffer_id));
1671                                }
1672                            }
1673                        }
1674
1675                        if let Some(worktree) = worktree {
1676                            let worktree_id = worktree.id();
1677                            let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
1678
1679                            match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
1680                                Some(entry) => {
1681                                    let mut traversal = worktree.traverse_from_path(
1682                                        true,
1683                                        true,
1684                                        true,
1685                                        entry.path.as_ref(),
1686                                    );
1687
1688                                    let mut entries_to_add = HashSet::default();
1689                                    worktree_excerpts
1690                                        .entry(worktree_id)
1691                                        .or_default()
1692                                        .insert(entry.id, (buffer_id, excerpts));
1693                                    let mut current_entry = entry;
1694                                    loop {
1695                                        if current_entry.is_dir() {
1696                                            let is_root =
1697                                                worktree.root_entry().map(|entry| entry.id)
1698                                                    == Some(current_entry.id);
1699                                            if is_root {
1700                                                root_entries.insert(current_entry.id);
1701                                                if auto_fold_dirs {
1702                                                    unfolded_dirs.insert(current_entry.id);
1703                                                }
1704                                            }
1705                                            if is_new {
1706                                                new_collapsed_entries.remove(&CollapsedEntry::Dir(
1707                                                    worktree_id,
1708                                                    current_entry.id,
1709                                                ));
1710                                            }
1711                                        }
1712
1713                                        let new_entry_added = entries_to_add.insert(current_entry);
1714                                        if new_entry_added && traversal.back_to_parent() {
1715                                            if let Some(parent_entry) = traversal.entry() {
1716                                                current_entry = parent_entry.clone();
1717                                                continue;
1718                                            }
1719                                        }
1720                                        break;
1721                                    }
1722                                    new_worktree_entries
1723                                        .entry(worktree_id)
1724                                        .or_insert_with(|| (worktree.clone(), HashSet::default()))
1725                                        .1
1726                                        .extend(entries_to_add);
1727                                }
1728                                None => {
1729                                    if processed_external_buffers.insert(buffer_id) {
1730                                        external_excerpts
1731                                            .entry(buffer_id)
1732                                            .or_insert_with(|| Vec::new())
1733                                            .extend(excerpts);
1734                                    }
1735                                }
1736                            }
1737                        } else if processed_external_buffers.insert(buffer_id) {
1738                            external_excerpts
1739                                .entry(buffer_id)
1740                                .or_insert_with(|| Vec::new())
1741                                .extend(excerpts);
1742                        }
1743                    }
1744
1745                    let mut new_children_count =
1746                        HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
1747
1748                    let worktree_entries = new_worktree_entries
1749                        .into_iter()
1750                        .map(|(worktree_id, (worktree_snapshot, entries))| {
1751                            let mut entries = entries.into_iter().collect::<Vec<_>>();
1752                            // For a proper git status propagation, we have to keep the entries sorted lexicographically.
1753                            entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
1754                            worktree_snapshot.propagate_git_statuses(&mut entries);
1755                            project::sort_worktree_entries(&mut entries);
1756                            (worktree_id, entries)
1757                        })
1758                        .flat_map(|(worktree_id, entries)| {
1759                            {
1760                                entries
1761                                    .into_iter()
1762                                    .filter_map(|entry| {
1763                                        if auto_fold_dirs {
1764                                            if let Some(parent) = entry.path.parent() {
1765                                                let children = new_children_count
1766                                                    .entry(worktree_id)
1767                                                    .or_default()
1768                                                    .entry(Arc::from(parent))
1769                                                    .or_default();
1770                                                if entry.is_dir() {
1771                                                    children.dirs += 1;
1772                                                } else {
1773                                                    children.files += 1;
1774                                                }
1775                                            }
1776                                        }
1777
1778                                        if entry.is_dir() {
1779                                            Some(FsEntry::Directory(worktree_id, entry))
1780                                        } else {
1781                                            let (buffer_id, excerpts) = worktree_excerpts
1782                                                .get_mut(&worktree_id)
1783                                                .and_then(|worktree_excerpts| {
1784                                                    worktree_excerpts.remove(&entry.id)
1785                                                })?;
1786                                            Some(FsEntry::File(
1787                                                worktree_id,
1788                                                entry,
1789                                                buffer_id,
1790                                                excerpts,
1791                                            ))
1792                                        }
1793                                    })
1794                                    .collect::<Vec<_>>()
1795                            }
1796                        })
1797                        .collect::<Vec<_>>();
1798
1799                    let mut visited_dirs = Vec::new();
1800                    let mut new_depth_map = HashMap::default();
1801                    let new_visible_entries = external_excerpts
1802                        .into_iter()
1803                        .sorted_by_key(|(id, _)| *id)
1804                        .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts))
1805                        .chain(worktree_entries)
1806                        .filter(|visible_item| {
1807                            match visible_item {
1808                                FsEntry::Directory(worktree_id, dir_entry) => {
1809                                    let parent_id = back_to_common_visited_parent(
1810                                        &mut visited_dirs,
1811                                        worktree_id,
1812                                        dir_entry,
1813                                    );
1814
1815                                    let depth = if root_entries.contains(&dir_entry.id) {
1816                                        0
1817                                    } else {
1818                                        if auto_fold_dirs {
1819                                            let children = new_children_count
1820                                                .get(&worktree_id)
1821                                                .and_then(|children_count| {
1822                                                    children_count.get(&dir_entry.path)
1823                                                })
1824                                                .copied()
1825                                                .unwrap_or_default();
1826
1827                                            if !children.may_be_fold_part()
1828                                                || (children.dirs == 0
1829                                                    && visited_dirs
1830                                                        .last()
1831                                                        .map(|(parent_dir_id, _)| {
1832                                                            new_unfolded_dirs
1833                                                                .get(&worktree_id)
1834                                                                .map_or(true, |unfolded_dirs| {
1835                                                                    unfolded_dirs
1836                                                                        .contains(&parent_dir_id)
1837                                                                })
1838                                                        })
1839                                                        .unwrap_or(true))
1840                                            {
1841                                                new_unfolded_dirs
1842                                                    .entry(*worktree_id)
1843                                                    .or_default()
1844                                                    .insert(dir_entry.id);
1845                                            }
1846                                        }
1847
1848                                        parent_id
1849                                            .and_then(|(worktree_id, id)| {
1850                                                new_depth_map.get(&(worktree_id, id)).copied()
1851                                            })
1852                                            .unwrap_or(0)
1853                                            + 1
1854                                    };
1855                                    visited_dirs.push((dir_entry.id, dir_entry.path.clone()));
1856                                    new_depth_map.insert((*worktree_id, dir_entry.id), depth);
1857                                }
1858                                FsEntry::File(worktree_id, file_entry, ..) => {
1859                                    let parent_id = back_to_common_visited_parent(
1860                                        &mut visited_dirs,
1861                                        worktree_id,
1862                                        file_entry,
1863                                    );
1864                                    let depth = if root_entries.contains(&file_entry.id) {
1865                                        0
1866                                    } else {
1867                                        parent_id
1868                                            .and_then(|(worktree_id, id)| {
1869                                                new_depth_map.get(&(worktree_id, id)).copied()
1870                                            })
1871                                            .unwrap_or(0)
1872                                            + 1
1873                                    };
1874                                    new_depth_map.insert((*worktree_id, file_entry.id), depth);
1875                                }
1876                                FsEntry::ExternalFile(..) => {
1877                                    visited_dirs.clear();
1878                                }
1879                            }
1880
1881                            true
1882                        })
1883                        .collect::<Vec<_>>();
1884
1885                    anyhow::Ok((
1886                        new_collapsed_entries,
1887                        new_unfolded_dirs,
1888                        new_visible_entries,
1889                        new_depth_map,
1890                        new_children_count,
1891                    ))
1892                })
1893                .await
1894                .log_err()
1895            else {
1896                return;
1897            };
1898
1899            outline_panel
1900                .update(&mut cx, |outline_panel, cx| {
1901                    outline_panel.updating_fs_entries = false;
1902                    outline_panel.excerpts = new_excerpts;
1903                    outline_panel.collapsed_entries = new_collapsed_entries;
1904                    outline_panel.unfolded_dirs = new_unfolded_dirs;
1905                    outline_panel.fs_entries = new_fs_entries;
1906                    outline_panel.fs_entries_depth = new_depth_map;
1907                    outline_panel.fs_children_count = new_children_count;
1908                    outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
1909                    if new_selected_entry.is_some() {
1910                        outline_panel.selected_entry = new_selected_entry;
1911                    }
1912                    outline_panel.fetch_outdated_outlines(cx);
1913                    outline_panel.autoscroll(cx);
1914                    cx.notify();
1915                })
1916                .ok();
1917        });
1918    }
1919
1920    fn replace_visible_entries(
1921        &mut self,
1922        new_active_editor: View<Editor>,
1923        cx: &mut ViewContext<Self>,
1924    ) {
1925        let new_selected_entry = self.location_for_editor_selection(&new_active_editor, cx);
1926        self.clear_previous(cx);
1927        self.active_item = Some(ActiveItem {
1928            item_id: new_active_editor.item_id(),
1929            _editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, cx),
1930            active_editor: new_active_editor.downgrade(),
1931        });
1932        let new_entries =
1933            HashSet::from_iter(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
1934        self.update_fs_entries(
1935            &new_active_editor,
1936            new_entries,
1937            new_selected_entry,
1938            None,
1939            cx,
1940        );
1941    }
1942
1943    fn clear_previous(&mut self, cx: &mut WindowContext<'_>) {
1944        self.filter_editor.update(cx, |editor, cx| editor.clear(cx));
1945        self.collapsed_entries.clear();
1946        self.unfolded_dirs.clear();
1947        self.selected_entry = None;
1948        self.fs_entries_update_task = Task::ready(());
1949        self.cached_entries_update_task = Task::ready(());
1950        self.active_item = None;
1951        self.fs_entries.clear();
1952        self.fs_entries_depth.clear();
1953        self.fs_children_count.clear();
1954        self.outline_fetch_tasks.clear();
1955        self.excerpts.clear();
1956        self.cached_entries_with_depth = Vec::new();
1957    }
1958
1959    fn location_for_editor_selection(
1960        &mut self,
1961        editor: &View<Editor>,
1962        cx: &mut ViewContext<Self>,
1963    ) -> Option<EntryOwned> {
1964        let selection = editor
1965            .read(cx)
1966            .selections
1967            .newest::<language::Point>(cx)
1968            .head();
1969        let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx));
1970        let multi_buffer = editor.read(cx).buffer();
1971        let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
1972        let (excerpt_id, buffer, _) = editor
1973            .read(cx)
1974            .buffer()
1975            .read(cx)
1976            .excerpt_containing(selection, cx)?;
1977        let buffer_id = buffer.read(cx).remote_id();
1978        let selection_display_point = selection.to_display_point(&editor_snapshot);
1979
1980        let excerpt_outlines = self
1981            .excerpts
1982            .get(&buffer_id)
1983            .and_then(|excerpts| excerpts.get(&excerpt_id))
1984            .into_iter()
1985            .flat_map(|excerpt| excerpt.iter_outlines())
1986            .flat_map(|outline| {
1987                let start = multi_buffer_snapshot
1988                    .anchor_in_excerpt(excerpt_id, outline.range.start)?
1989                    .to_display_point(&editor_snapshot);
1990                let end = multi_buffer_snapshot
1991                    .anchor_in_excerpt(excerpt_id, outline.range.end)?
1992                    .to_display_point(&editor_snapshot);
1993                Some((start..end, outline))
1994            })
1995            .collect::<Vec<_>>();
1996
1997        let mut matching_outline_indices = Vec::new();
1998        let mut children = HashMap::default();
1999        let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
2000
2001        for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
2002            if outline_range
2003                .to_inclusive()
2004                .contains(&selection_display_point)
2005            {
2006                matching_outline_indices.push(i);
2007            } else if (outline_range.start.row()..outline_range.end.row())
2008                .to_inclusive()
2009                .contains(&selection_display_point.row())
2010            {
2011                matching_outline_indices.push(i);
2012            }
2013
2014            while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
2015                if parent_outline.depth >= outline.depth
2016                    || !parent_range.contains(&outline_range.start)
2017                {
2018                    parents_stack.pop();
2019                } else {
2020                    break;
2021                }
2022            }
2023            if let Some((_, _, parent_index)) = parents_stack.last_mut() {
2024                children
2025                    .entry(*parent_index)
2026                    .or_insert_with(Vec::new)
2027                    .push(i);
2028            }
2029            parents_stack.push((outline_range, outline, i));
2030        }
2031
2032        let outline_item = matching_outline_indices
2033            .into_iter()
2034            .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
2035            .filter(|(i, _)| {
2036                children
2037                    .get(i)
2038                    .map(|children| {
2039                        children.iter().all(|child_index| {
2040                            excerpt_outlines
2041                                .get(*child_index)
2042                                .map(|(child_range, _)| child_range.start > selection_display_point)
2043                                .unwrap_or(false)
2044                        })
2045                    })
2046                    .unwrap_or(true)
2047            })
2048            .min_by_key(|(_, (outline_range, outline))| {
2049                let distance_from_start = if outline_range.start > selection_display_point {
2050                    outline_range.start - selection_display_point
2051                } else {
2052                    selection_display_point - outline_range.start
2053                };
2054                let distance_from_end = if outline_range.end > selection_display_point {
2055                    outline_range.end - selection_display_point
2056                } else {
2057                    selection_display_point - outline_range.end
2058                };
2059
2060                (
2061                    cmp::Reverse(outline.depth),
2062                    distance_from_start + distance_from_end,
2063                )
2064            })
2065            .map(|(_, (_, outline))| *outline)
2066            .cloned();
2067
2068        let closest_container = match outline_item {
2069            Some(outline) => EntryOwned::Outline(buffer_id, excerpt_id, outline),
2070            None => self
2071                .cached_entries_with_depth
2072                .iter()
2073                .rev()
2074                .find_map(|cached_entry| match &cached_entry.entry {
2075                    EntryOwned::Excerpt(entry_buffer_id, entry_excerpt_id, _) => {
2076                        if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id {
2077                            Some(cached_entry.entry.clone())
2078                        } else {
2079                            None
2080                        }
2081                    }
2082                    EntryOwned::Entry(
2083                        FsEntry::ExternalFile(file_buffer_id, file_excerpts)
2084                        | FsEntry::File(_, _, file_buffer_id, file_excerpts),
2085                    ) => {
2086                        if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
2087                            Some(cached_entry.entry.clone())
2088                        } else {
2089                            None
2090                        }
2091                    }
2092                    _ => None,
2093                })?,
2094        };
2095        Some(closest_container)
2096    }
2097
2098    fn fetch_outdated_outlines(&mut self, cx: &mut ViewContext<Self>) {
2099        let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
2100        if excerpt_fetch_ranges.is_empty() {
2101            return;
2102        }
2103
2104        let syntax_theme = cx.theme().syntax().clone();
2105        for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
2106            for (excerpt_id, excerpt_range) in excerpt_ranges {
2107                let syntax_theme = syntax_theme.clone();
2108                let buffer_snapshot = buffer_snapshot.clone();
2109                self.outline_fetch_tasks.insert(
2110                    (buffer_id, excerpt_id),
2111                    cx.spawn(|outline_panel, mut cx| async move {
2112                        let fetched_outlines = cx
2113                            .background_executor()
2114                            .spawn(async move {
2115                                buffer_snapshot
2116                                    .outline_items_containing(
2117                                        excerpt_range.context,
2118                                        false,
2119                                        Some(&syntax_theme),
2120                                    )
2121                                    .unwrap_or_default()
2122                            })
2123                            .await;
2124                        outline_panel
2125                            .update(&mut cx, |outline_panel, cx| {
2126                                if let Some(excerpt) = outline_panel
2127                                    .excerpts
2128                                    .entry(buffer_id)
2129                                    .or_default()
2130                                    .get_mut(&excerpt_id)
2131                                {
2132                                    excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
2133                                }
2134                                outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
2135                            })
2136                            .ok();
2137                    }),
2138                );
2139            }
2140        }
2141    }
2142
2143    fn is_singleton_active(&self, cx: &AppContext) -> bool {
2144        self.active_item
2145            .as_ref()
2146            .and_then(|active_item| {
2147                Some(
2148                    active_item
2149                        .active_editor
2150                        .upgrade()?
2151                        .read(cx)
2152                        .buffer()
2153                        .read(cx)
2154                        .is_singleton(),
2155                )
2156            })
2157            .unwrap_or(false)
2158    }
2159
2160    fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
2161        self.outline_fetch_tasks.clear();
2162        let mut ids = ids.into_iter().collect::<HashSet<_>>();
2163        for excerpts in self.excerpts.values_mut() {
2164            ids.retain(|id| {
2165                if let Some(excerpt) = excerpts.get_mut(id) {
2166                    excerpt.invalidate_outlines();
2167                    false
2168                } else {
2169                    true
2170                }
2171            });
2172            if ids.is_empty() {
2173                break;
2174            }
2175        }
2176    }
2177
2178    fn excerpt_fetch_ranges(
2179        &self,
2180        cx: &AppContext,
2181    ) -> HashMap<
2182        BufferId,
2183        (
2184            BufferSnapshot,
2185            HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
2186        ),
2187    > {
2188        self.fs_entries
2189            .iter()
2190            .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
2191                match fs_entry {
2192                    FsEntry::File(_, _, buffer_id, file_excerpts)
2193                    | FsEntry::ExternalFile(buffer_id, file_excerpts) => {
2194                        let excerpts = self.excerpts.get(&buffer_id);
2195                        for &file_excerpt in file_excerpts {
2196                            if let Some(excerpt) = excerpts
2197                                .and_then(|excerpts| excerpts.get(&file_excerpt))
2198                                .filter(|excerpt| excerpt.should_fetch_outlines())
2199                            {
2200                                match excerpts_to_fetch.entry(*buffer_id) {
2201                                    hash_map::Entry::Occupied(mut o) => {
2202                                        o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
2203                                    }
2204                                    hash_map::Entry::Vacant(v) => {
2205                                        if let Some(buffer_snapshot) =
2206                                            self.buffer_snapshot_for_id(*buffer_id, cx)
2207                                        {
2208                                            v.insert((buffer_snapshot, HashMap::default()))
2209                                                .1
2210                                                .insert(file_excerpt, excerpt.range.clone());
2211                                        }
2212                                    }
2213                                }
2214                            }
2215                        }
2216                    }
2217                    FsEntry::Directory(..) => {}
2218                }
2219                excerpts_to_fetch
2220            })
2221    }
2222
2223    fn buffer_snapshot_for_id(
2224        &self,
2225        buffer_id: BufferId,
2226        cx: &AppContext,
2227    ) -> Option<BufferSnapshot> {
2228        let editor = self.active_item.as_ref()?.active_editor.upgrade()?;
2229        Some(
2230            editor
2231                .read(cx)
2232                .buffer()
2233                .read(cx)
2234                .buffer(buffer_id)?
2235                .read(cx)
2236                .snapshot(),
2237        )
2238    }
2239
2240    fn abs_path(&self, entry: &EntryOwned, cx: &AppContext) -> Option<PathBuf> {
2241        match entry {
2242            EntryOwned::Entry(
2243                FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _),
2244            ) => self
2245                .buffer_snapshot_for_id(*buffer_id, cx)
2246                .and_then(|buffer_snapshot| {
2247                    let file = File::from_dyn(buffer_snapshot.file())?;
2248                    file.worktree.read(cx).absolutize(&file.path).ok()
2249                }),
2250            EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => self
2251                .project
2252                .read(cx)
2253                .worktree_for_id(*worktree_id, cx)?
2254                .read(cx)
2255                .absolutize(&entry.path)
2256                .ok(),
2257            EntryOwned::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
2258                self.project
2259                    .read(cx)
2260                    .worktree_for_id(*worktree_id, cx)
2261                    .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
2262            }),
2263            EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
2264        }
2265    }
2266
2267    fn relative_path(&self, entry: &FsEntry, cx: &AppContext) -> Option<Arc<Path>> {
2268        match entry {
2269            FsEntry::ExternalFile(buffer_id, _) => {
2270                let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
2271                Some(buffer_snapshot.file()?.path().clone())
2272            }
2273            FsEntry::Directory(_, entry) => Some(entry.path.clone()),
2274            FsEntry::File(_, entry, ..) => Some(entry.path.clone()),
2275        }
2276    }
2277
2278    fn update_cached_entries(
2279        &mut self,
2280        debounce: Option<Duration>,
2281        cx: &mut ViewContext<OutlinePanel>,
2282    ) {
2283        let is_singleton = self.is_singleton_active(cx);
2284        let query = self.query(cx);
2285        self.cached_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
2286            if let Some(debounce) = debounce {
2287                cx.background_executor().timer(debounce).await;
2288            }
2289            let Some(new_cached_entries) = outline_panel
2290                .update(&mut cx, |outline_panel, cx| {
2291                    outline_panel.generate_cached_entries(is_singleton, query, cx)
2292                })
2293                .ok()
2294            else {
2295                return;
2296            };
2297            let new_cached_entries = new_cached_entries.await;
2298            outline_panel
2299                .update(&mut cx, |outline_panel, cx| {
2300                    outline_panel.cached_entries_with_depth = new_cached_entries;
2301                    cx.notify();
2302                })
2303                .ok();
2304        });
2305    }
2306
2307    fn generate_cached_entries(
2308        &self,
2309        is_singleton: bool,
2310        query: Option<String>,
2311        cx: &mut ViewContext<'_, Self>,
2312    ) -> Task<Vec<CachedEntry>> {
2313        let project = self.project.clone();
2314        cx.spawn(|outline_panel, mut cx| async move {
2315            let mut entries = Vec::new();
2316            let mut match_candidates = Vec::new();
2317
2318            let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| {
2319                let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2320                let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec<Entry>)>;
2321                let track_matches = query.is_some();
2322                let mut parent_dirs = Vec::<(&Path, bool, bool, usize)>::new();
2323
2324                for entry in &outline_panel.fs_entries {
2325                    let is_expanded = outline_panel.is_expanded(entry);
2326                    let (depth, should_add) = match entry {
2327                        FsEntry::Directory(worktree_id, dir_entry) => {
2328                            let is_root = project
2329                                .read(cx)
2330                                .worktree_for_id(*worktree_id, cx)
2331                                .map_or(false, |worktree| {
2332                                    worktree.read(cx).root_entry() == Some(dir_entry)
2333                                });
2334                            let folded = auto_fold_dirs
2335                                && !is_root
2336                                && outline_panel
2337                                    .unfolded_dirs
2338                                    .get(worktree_id)
2339                                    .map_or(true, |unfolded_dirs| {
2340                                        !unfolded_dirs.contains(&dir_entry.id)
2341                                    });
2342                            let fs_depth = outline_panel
2343                                .fs_entries_depth
2344                                .get(&(*worktree_id, dir_entry.id))
2345                                .copied()
2346                                .unwrap_or(0);
2347                            while let Some(&(previous_path, ..)) = parent_dirs.last() {
2348                                if dir_entry.path.starts_with(previous_path) {
2349                                    break;
2350                                }
2351                                parent_dirs.pop();
2352                            }
2353                            let auto_fold = match parent_dirs.last() {
2354                                Some((parent_path, parent_folded, _, _)) => {
2355                                    *parent_folded
2356                                        && Some(*parent_path) == dir_entry.path.parent()
2357                                        && outline_panel
2358                                            .fs_children_count
2359                                            .get(worktree_id)
2360                                            .and_then(|entries| entries.get(&dir_entry.path))
2361                                            .copied()
2362                                            .unwrap_or_default()
2363                                            .may_be_fold_part()
2364                                }
2365                                None => false,
2366                            };
2367                            let folded = folded || auto_fold;
2368                            let (depth, parent_expanded) = match parent_dirs.last() {
2369                                Some(&(_, previous_folded, previous_expanded, previous_depth)) => {
2370                                    let new_depth = if folded && previous_folded {
2371                                        previous_depth
2372                                    } else {
2373                                        previous_depth + 1
2374                                    };
2375                                    parent_dirs.push((
2376                                        &dir_entry.path,
2377                                        folded,
2378                                        previous_expanded && is_expanded,
2379                                        new_depth,
2380                                    ));
2381                                    (new_depth, previous_expanded)
2382                                }
2383                                None => {
2384                                    parent_dirs.push((
2385                                        &dir_entry.path,
2386                                        folded,
2387                                        is_expanded,
2388                                        fs_depth,
2389                                    ));
2390                                    (fs_depth, true)
2391                                }
2392                            };
2393
2394                            if let Some((folded_depth, folded_worktree_id, mut folded_dirs)) =
2395                                folded_dirs_entry.take()
2396                            {
2397                                if folded
2398                                    && worktree_id == &folded_worktree_id
2399                                    && dir_entry.path.parent()
2400                                        == folded_dirs.last().map(|entry| entry.path.as_ref())
2401                                {
2402                                    folded_dirs.push(dir_entry.clone());
2403                                    folded_dirs_entry =
2404                                        Some((folded_depth, folded_worktree_id, folded_dirs))
2405                                } else {
2406                                    if parent_expanded || query.is_some() {
2407                                        let new_folded_dirs =
2408                                            EntryOwned::FoldedDirs(folded_worktree_id, folded_dirs);
2409                                        outline_panel.push_entry(
2410                                            &mut entries,
2411                                            &mut match_candidates,
2412                                            track_matches,
2413                                            new_folded_dirs,
2414                                            folded_depth,
2415                                            cx,
2416                                        );
2417                                    }
2418                                    folded_dirs_entry =
2419                                        Some((depth, *worktree_id, vec![dir_entry.clone()]))
2420                                }
2421                            } else if folded {
2422                                folded_dirs_entry =
2423                                    Some((depth, *worktree_id, vec![dir_entry.clone()]));
2424                            }
2425
2426                            let should_add = parent_expanded && folded_dirs_entry.is_none();
2427                            (depth, should_add)
2428                        }
2429                        FsEntry::ExternalFile(..) => {
2430                            if let Some((folded_depth, worktree_id, folded_dirs)) =
2431                                folded_dirs_entry.take()
2432                            {
2433                                let parent_expanded = parent_dirs
2434                                    .iter()
2435                                    .rev()
2436                                    .find(|(parent_path, ..)| {
2437                                        folded_dirs
2438                                            .iter()
2439                                            .all(|entry| entry.path.as_ref() != *parent_path)
2440                                    })
2441                                    .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
2442                                if parent_expanded || query.is_some() {
2443                                    outline_panel.push_entry(
2444                                        &mut entries,
2445                                        &mut match_candidates,
2446                                        track_matches,
2447                                        EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2448                                        folded_depth,
2449                                        cx,
2450                                    );
2451                                }
2452                            }
2453                            parent_dirs.clear();
2454                            (0, true)
2455                        }
2456                        FsEntry::File(worktree_id, file_entry, ..) => {
2457                            if let Some((folded_depth, worktree_id, folded_dirs)) =
2458                                folded_dirs_entry.take()
2459                            {
2460                                let parent_expanded = parent_dirs
2461                                    .iter()
2462                                    .rev()
2463                                    .find(|(parent_path, ..)| {
2464                                        folded_dirs
2465                                            .iter()
2466                                            .all(|entry| entry.path.as_ref() != *parent_path)
2467                                    })
2468                                    .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
2469                                if parent_expanded || query.is_some() {
2470                                    outline_panel.push_entry(
2471                                        &mut entries,
2472                                        &mut match_candidates,
2473                                        track_matches,
2474                                        EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2475                                        folded_depth,
2476                                        cx,
2477                                    );
2478                                }
2479                            }
2480
2481                            let fs_depth = outline_panel
2482                                .fs_entries_depth
2483                                .get(&(*worktree_id, file_entry.id))
2484                                .copied()
2485                                .unwrap_or(0);
2486                            while let Some(&(previous_path, ..)) = parent_dirs.last() {
2487                                if file_entry.path.starts_with(previous_path) {
2488                                    break;
2489                                }
2490                                parent_dirs.pop();
2491                            }
2492                            let (depth, should_add) = match parent_dirs.last() {
2493                                Some(&(_, _, previous_expanded, previous_depth)) => {
2494                                    let new_depth = previous_depth + 1;
2495                                    (new_depth, previous_expanded)
2496                                }
2497                                None => (fs_depth, true),
2498                            };
2499                            (depth, should_add)
2500                        }
2501                    };
2502
2503                    if !is_singleton
2504                        && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
2505                    {
2506                        outline_panel.push_entry(
2507                            &mut entries,
2508                            &mut match_candidates,
2509                            track_matches,
2510                            EntryOwned::Entry(entry.clone()),
2511                            depth,
2512                            cx,
2513                        );
2514                    }
2515
2516                    let excerpts_to_consider =
2517                        if is_singleton || query.is_some() || (should_add && is_expanded) {
2518                            match entry {
2519                                FsEntry::File(_, _, buffer_id, entry_excerpts) => {
2520                                    Some((*buffer_id, entry_excerpts))
2521                                }
2522                                FsEntry::ExternalFile(buffer_id, entry_excerpts) => {
2523                                    Some((*buffer_id, entry_excerpts))
2524                                }
2525                                _ => None,
2526                            }
2527                        } else {
2528                            None
2529                        };
2530                    if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
2531                        if let Some(excerpts) = outline_panel.excerpts.get(&buffer_id) {
2532                            for &entry_excerpt in entry_excerpts {
2533                                let Some(excerpt) = excerpts.get(&entry_excerpt) else {
2534                                    continue;
2535                                };
2536                                let excerpt_depth = depth + 1;
2537                                outline_panel.push_entry(
2538                                    &mut entries,
2539                                    &mut match_candidates,
2540                                    track_matches,
2541                                    EntryOwned::Excerpt(
2542                                        buffer_id,
2543                                        entry_excerpt,
2544                                        excerpt.range.clone(),
2545                                    ),
2546                                    excerpt_depth,
2547                                    cx,
2548                                );
2549
2550                                let mut outline_base_depth = excerpt_depth + 1;
2551                                if is_singleton {
2552                                    outline_base_depth = 0;
2553                                    entries.clear();
2554                                    match_candidates.clear();
2555                                } else if query.is_none()
2556                                    && outline_panel.collapsed_entries.contains(
2557                                        &CollapsedEntry::Excerpt(buffer_id, entry_excerpt),
2558                                    )
2559                                {
2560                                    continue;
2561                                }
2562
2563                                for outline in excerpt.iter_outlines() {
2564                                    outline_panel.push_entry(
2565                                        &mut entries,
2566                                        &mut match_candidates,
2567                                        track_matches,
2568                                        EntryOwned::Outline(
2569                                            buffer_id,
2570                                            entry_excerpt,
2571                                            outline.clone(),
2572                                        ),
2573                                        outline_base_depth + outline.depth,
2574                                        cx,
2575                                    );
2576                                }
2577                                if is_singleton && entries.is_empty() {
2578                                    outline_panel.push_entry(
2579                                        &mut entries,
2580                                        &mut match_candidates,
2581                                        track_matches,
2582                                        EntryOwned::Entry(entry.clone()),
2583                                        0,
2584                                        cx,
2585                                    );
2586                                }
2587                            }
2588                        }
2589                    }
2590                }
2591
2592                if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
2593                    let parent_expanded = parent_dirs
2594                        .iter()
2595                        .rev()
2596                        .find(|(parent_path, ..)| {
2597                            folded_dirs
2598                                .iter()
2599                                .all(|entry| entry.path.as_ref() != *parent_path)
2600                        })
2601                        .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
2602                    if parent_expanded || query.is_some() {
2603                        outline_panel.push_entry(
2604                            &mut entries,
2605                            &mut match_candidates,
2606                            track_matches,
2607                            EntryOwned::FoldedDirs(worktree_id, folded_dirs),
2608                            folded_depth,
2609                            cx,
2610                        );
2611                    }
2612                }
2613            }) else {
2614                return Vec::new();
2615            };
2616
2617            let Some(query) = query else {
2618                return entries;
2619            };
2620            let mut matched_ids = match_strings(
2621                &match_candidates,
2622                &query,
2623                true,
2624                usize::MAX,
2625                &AtomicBool::default(),
2626                cx.background_executor().clone(),
2627            )
2628            .await
2629            .into_iter()
2630            .map(|string_match| (string_match.candidate_id, string_match))
2631            .collect::<HashMap<_, _>>();
2632
2633            let mut id = 0;
2634            entries.retain_mut(|cached_entry| {
2635                let retain = match matched_ids.remove(&id) {
2636                    Some(string_match) => {
2637                        cached_entry.string_match = Some(string_match);
2638                        true
2639                    }
2640                    None => false,
2641                };
2642                id += 1;
2643                retain
2644            });
2645
2646            entries
2647        })
2648    }
2649
2650    fn push_entry(
2651        &self,
2652        entries: &mut Vec<CachedEntry>,
2653        match_candidates: &mut Vec<StringMatchCandidate>,
2654        track_matches: bool,
2655        entry: EntryOwned,
2656        depth: usize,
2657        cx: &AppContext,
2658    ) {
2659        if track_matches {
2660            let id = entries.len();
2661            match &entry {
2662                EntryOwned::Entry(fs_entry) => {
2663                    if let Some(file_name) =
2664                        self.relative_path(fs_entry, cx).as_deref().map(file_name)
2665                    {
2666                        match_candidates.push(StringMatchCandidate {
2667                            id,
2668                            string: file_name.to_string(),
2669                            char_bag: file_name.chars().collect(),
2670                        });
2671                    }
2672                }
2673                EntryOwned::FoldedDirs(worktree_id, entries) => {
2674                    let dir_names = self.dir_names_string(entries, *worktree_id, cx);
2675                    {
2676                        match_candidates.push(StringMatchCandidate {
2677                            id,
2678                            string: dir_names.to_string(),
2679                            char_bag: dir_names.chars().collect(),
2680                        });
2681                    }
2682                }
2683                EntryOwned::Outline(_, _, outline) => match_candidates.push(StringMatchCandidate {
2684                    id,
2685                    string: outline.text.clone(),
2686                    char_bag: outline.text.chars().collect(),
2687                }),
2688                EntryOwned::Excerpt(..) => {}
2689            }
2690        }
2691        entries.push(CachedEntry {
2692            depth,
2693            entry,
2694            string_match: None,
2695        });
2696    }
2697
2698    fn dir_names_string(
2699        &self,
2700        entries: &[Entry],
2701        worktree_id: WorktreeId,
2702        cx: &AppContext,
2703    ) -> String {
2704        let dir_names_segment = entries
2705            .iter()
2706            .map(|entry| self.entry_name(&worktree_id, entry, cx))
2707            .collect::<PathBuf>();
2708        dir_names_segment.to_string_lossy().to_string()
2709    }
2710
2711    fn query(&self, cx: &AppContext) -> Option<String> {
2712        let query = self.filter_editor.read(cx).text(cx);
2713        if query.trim().is_empty() {
2714            None
2715        } else {
2716            Some(query)
2717        }
2718    }
2719
2720    fn is_expanded(&self, entry: &FsEntry) -> bool {
2721        let entry_to_check = match entry {
2722            FsEntry::ExternalFile(buffer_id, _) => CollapsedEntry::ExternalFile(*buffer_id),
2723            FsEntry::File(worktree_id, _, buffer_id, _) => {
2724                CollapsedEntry::File(*worktree_id, *buffer_id)
2725            }
2726            FsEntry::Directory(worktree_id, entry) => CollapsedEntry::Dir(*worktree_id, entry.id),
2727        };
2728        !self.collapsed_entries.contains(&entry_to_check)
2729    }
2730}
2731
2732fn back_to_common_visited_parent(
2733    visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
2734    worktree_id: &WorktreeId,
2735    new_entry: &Entry,
2736) -> Option<(WorktreeId, ProjectEntryId)> {
2737    while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
2738        match new_entry.path.parent() {
2739            Some(parent_path) => {
2740                if parent_path == visited_path.as_ref() {
2741                    return Some((*worktree_id, *visited_dir_id));
2742                }
2743            }
2744            None => {
2745                break;
2746            }
2747        }
2748        visited_dirs.pop();
2749    }
2750    None
2751}
2752
2753fn file_name(path: &Path) -> String {
2754    let mut current_path = path;
2755    loop {
2756        if let Some(file_name) = current_path.file_name() {
2757            return file_name.to_string_lossy().into_owned();
2758        }
2759        match current_path.parent() {
2760            Some(parent) => current_path = parent,
2761            None => return path.to_string_lossy().into_owned(),
2762        }
2763    }
2764}
2765
2766impl Panel for OutlinePanel {
2767    fn persistent_name() -> &'static str {
2768        "Outline Panel"
2769    }
2770
2771    fn position(&self, cx: &WindowContext) -> DockPosition {
2772        match OutlinePanelSettings::get_global(cx).dock {
2773            OutlinePanelDockPosition::Left => DockPosition::Left,
2774            OutlinePanelDockPosition::Right => DockPosition::Right,
2775        }
2776    }
2777
2778    fn position_is_valid(&self, position: DockPosition) -> bool {
2779        matches!(position, DockPosition::Left | DockPosition::Right)
2780    }
2781
2782    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
2783        settings::update_settings_file::<OutlinePanelSettings>(
2784            self.fs.clone(),
2785            cx,
2786            move |settings| {
2787                let dock = match position {
2788                    DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
2789                    DockPosition::Right => OutlinePanelDockPosition::Right,
2790                };
2791                settings.dock = Some(dock);
2792            },
2793        );
2794    }
2795
2796    fn size(&self, cx: &WindowContext) -> Pixels {
2797        self.width
2798            .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
2799    }
2800
2801    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
2802        self.width = size;
2803        self.serialize(cx);
2804        cx.notify();
2805    }
2806
2807    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
2808        OutlinePanelSettings::get_global(cx)
2809            .button
2810            .then(|| IconName::ListTree)
2811    }
2812
2813    fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
2814        Some("Outline Panel")
2815    }
2816
2817    fn toggle_action(&self) -> Box<dyn Action> {
2818        Box::new(ToggleFocus)
2819    }
2820
2821    fn starts_open(&self, _: &WindowContext) -> bool {
2822        self.active
2823    }
2824
2825    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2826        let old_active = self.active;
2827        self.active = active;
2828        if active && old_active != active {
2829            if let Some(active_editor) = self
2830                .active_item
2831                .as_ref()
2832                .and_then(|item| item.active_editor.upgrade())
2833            {
2834                if self.active_item.as_ref().map(|item| item.item_id)
2835                    == Some(active_editor.item_id())
2836                {
2837                    let new_selected_entry = self.location_for_editor_selection(&active_editor, cx);
2838                    self.update_fs_entries(
2839                        &active_editor,
2840                        HashSet::default(),
2841                        new_selected_entry,
2842                        None,
2843                        cx,
2844                    )
2845                } else {
2846                    self.replace_visible_entries(active_editor, cx);
2847                }
2848            }
2849        }
2850        self.serialize(cx);
2851    }
2852}
2853
2854impl FocusableView for OutlinePanel {
2855    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2856        self.filter_editor.focus_handle(cx).clone()
2857    }
2858}
2859
2860impl EventEmitter<Event> for OutlinePanel {}
2861
2862impl EventEmitter<PanelEvent> for OutlinePanel {}
2863
2864impl Render for OutlinePanel {
2865    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2866        let project = self.project.read(cx);
2867        let query = self.query(cx);
2868        let outline_panel = v_flex()
2869            .id("outline-panel")
2870            .size_full()
2871            .relative()
2872            .key_context(self.dispatch_context(cx))
2873            .on_action(cx.listener(Self::open))
2874            .on_action(cx.listener(Self::cancel))
2875            .on_action(cx.listener(Self::select_next))
2876            .on_action(cx.listener(Self::select_prev))
2877            .on_action(cx.listener(Self::select_first))
2878            .on_action(cx.listener(Self::select_last))
2879            .on_action(cx.listener(Self::select_parent))
2880            .on_action(cx.listener(Self::expand_selected_entry))
2881            .on_action(cx.listener(Self::collapse_selected_entry))
2882            .on_action(cx.listener(Self::expand_all_entries))
2883            .on_action(cx.listener(Self::collapse_all_entries))
2884            .on_action(cx.listener(Self::copy_path))
2885            .on_action(cx.listener(Self::copy_relative_path))
2886            .on_action(cx.listener(Self::unfold_directory))
2887            .on_action(cx.listener(Self::fold_directory))
2888            .when(project.is_local(), |el| {
2889                el.on_action(cx.listener(Self::reveal_in_finder))
2890                    .on_action(cx.listener(Self::open_in_terminal))
2891            })
2892            .on_mouse_down(
2893                MouseButton::Right,
2894                cx.listener(move |outline_panel, event: &MouseDownEvent, cx| {
2895                    if let Some(entry) = outline_panel.selected_entry.clone() {
2896                        outline_panel.deploy_context_menu(event.position, entry.to_ref_entry(), cx)
2897                    } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
2898                        outline_panel.deploy_context_menu(
2899                            event.position,
2900                            EntryRef::Entry(&entry),
2901                            cx,
2902                        )
2903                    }
2904                }),
2905            )
2906            .track_focus(&self.focus_handle);
2907
2908        if self.cached_entries_with_depth.is_empty() {
2909            let header = if self.updating_fs_entries {
2910                "Loading outlines"
2911            } else if query.is_some() {
2912                "No matches for query"
2913            } else {
2914                "No outlines available"
2915            };
2916
2917            outline_panel.child(
2918                v_flex()
2919                    .justify_center()
2920                    .size_full()
2921                    .child(h_flex().justify_center().child(Label::new(header)))
2922                    .when_some(query.clone(), |panel, query| {
2923                        panel.child(h_flex().justify_center().child(Label::new(query)))
2924                    })
2925                    .child(
2926                        h_flex()
2927                            .pt(Spacing::Small.rems(cx))
2928                            .justify_center()
2929                            .child({
2930                                let keystroke = match self.position(cx) {
2931                                    DockPosition::Left => {
2932                                        cx.keystroke_text_for(&workspace::ToggleLeftDock)
2933                                    }
2934                                    DockPosition::Bottom => {
2935                                        cx.keystroke_text_for(&workspace::ToggleBottomDock)
2936                                    }
2937                                    DockPosition::Right => {
2938                                        cx.keystroke_text_for(&workspace::ToggleRightDock)
2939                                    }
2940                                };
2941                                Label::new(format!("Toggle this panel with {keystroke}"))
2942                            }),
2943                    ),
2944            )
2945        } else {
2946            outline_panel.child({
2947                let items_len = self.cached_entries_with_depth.len();
2948                uniform_list(cx.view().clone(), "entries", items_len, {
2949                    move |outline_panel, range, cx| {
2950                        let entries = outline_panel.cached_entries_with_depth.get(range);
2951                        entries
2952                            .map(|entries| entries.to_vec())
2953                            .unwrap_or_default()
2954                            .into_iter()
2955                            .filter_map(|cached_entry| match cached_entry.entry {
2956                                EntryOwned::Entry(entry) => Some(outline_panel.render_entry(
2957                                    &entry,
2958                                    cached_entry.depth,
2959                                    cached_entry.string_match.as_ref(),
2960                                    cx,
2961                                )),
2962                                EntryOwned::FoldedDirs(worktree_id, entries) => {
2963                                    Some(outline_panel.render_folded_dirs(
2964                                        worktree_id,
2965                                        &entries,
2966                                        cached_entry.depth,
2967                                        cached_entry.string_match.as_ref(),
2968                                        cx,
2969                                    ))
2970                                }
2971                                EntryOwned::Excerpt(buffer_id, excerpt_id, excerpt) => {
2972                                    outline_panel.render_excerpt(
2973                                        buffer_id,
2974                                        excerpt_id,
2975                                        &excerpt,
2976                                        cached_entry.depth,
2977                                        cx,
2978                                    )
2979                                }
2980                                EntryOwned::Outline(buffer_id, excerpt_id, outline) => {
2981                                    Some(outline_panel.render_outline(
2982                                        buffer_id,
2983                                        excerpt_id,
2984                                        &outline,
2985                                        cached_entry.depth,
2986                                        cached_entry.string_match.as_ref(),
2987                                        cx,
2988                                    ))
2989                                }
2990                            })
2991                            .collect()
2992                    }
2993                })
2994                .size_full()
2995                .track_scroll(self.scroll_handle.clone())
2996            })
2997        }
2998        .children(self.context_menu.as_ref().map(|(menu, position, _)| {
2999            deferred(
3000                anchored()
3001                    .position(*position)
3002                    .anchor(gpui::AnchorCorner::TopLeft)
3003                    .child(menu.clone()),
3004            )
3005            .with_priority(1)
3006        }))
3007        .child(
3008            v_flex()
3009                .child(div().mx_2().border_primary(cx).border_t_1())
3010                .child(v_flex().p_2().child(self.filter_editor.clone())),
3011        )
3012    }
3013}
3014
3015fn subscribe_for_editor_events(
3016    editor: &View<Editor>,
3017    cx: &mut ViewContext<OutlinePanel>,
3018) -> Subscription {
3019    let debounce = Some(UPDATE_DEBOUNCE);
3020    cx.subscribe(
3021        editor,
3022        move |outline_panel, editor, e: &EditorEvent, cx| match e {
3023            EditorEvent::SelectionsChanged { local: true } => {
3024                outline_panel.reveal_entry_for_selection(&editor, cx);
3025                cx.notify();
3026            }
3027            EditorEvent::ExcerptsAdded { excerpts, .. } => {
3028                outline_panel.update_fs_entries(
3029                    &editor,
3030                    excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(),
3031                    None,
3032                    debounce,
3033                    cx,
3034                );
3035            }
3036            EditorEvent::ExcerptsRemoved { ids } => {
3037                let mut ids = ids.into_iter().collect::<HashSet<_>>();
3038                for excerpts in outline_panel.excerpts.values_mut() {
3039                    excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
3040                    if ids.is_empty() {
3041                        break;
3042                    }
3043                }
3044                outline_panel.update_fs_entries(&editor, HashSet::default(), None, debounce, cx);
3045            }
3046            EditorEvent::ExcerptsExpanded { ids } => {
3047                outline_panel.invalidate_outlines(ids);
3048                outline_panel.fetch_outdated_outlines(cx)
3049            }
3050            EditorEvent::ExcerptsEdited { ids } => {
3051                outline_panel.invalidate_outlines(ids);
3052                outline_panel.fetch_outdated_outlines(cx);
3053            }
3054            EditorEvent::Reparsed(buffer_id) => {
3055                if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
3056                    for (_, excerpt) in excerpts {
3057                        excerpt.invalidate_outlines();
3058                    }
3059                }
3060                outline_panel.fetch_outdated_outlines(cx);
3061            }
3062            _ => {}
3063        },
3064    )
3065}
3066
3067fn empty_icon() -> AnyElement {
3068    h_flex()
3069        .size(IconSize::default().rems())
3070        .invisible()
3071        .flex_none()
3072        .into_any_element()
3073}