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