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