outline_panel.rs

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