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