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