outline_panel.rs

   1mod outline_panel_settings;
   2
   3use std::{
   4    cell::OnceCell,
   5    cmp,
   6    ops::Range,
   7    path::{Path, PathBuf},
   8    sync::{atomic::AtomicBool, Arc},
   9    time::Duration,
  10    u32,
  11};
  12
  13use anyhow::Context;
  14use collections::{hash_map, BTreeSet, HashMap, HashSet};
  15use db::kvp::KEY_VALUE_STORE;
  16use editor::{
  17    display_map::ToDisplayPoint,
  18    items::{entry_git_aware_label_color, entry_label_color},
  19    scroll::{Autoscroll, ScrollAnchor},
  20    AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorMode, ExcerptId, ExcerptRange,
  21    MultiBufferSnapshot, RangeToAnchorExt,
  22};
  23use file_icons::FileIcons;
  24use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
  25use gpui::{
  26    actions, anchored, deferred, div, impl_actions, px, uniform_list, Action, AnyElement,
  27    AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId,
  28    EventEmitter, FocusHandle, FocusableView, HighlightStyle, InteractiveElement, IntoElement,
  29    KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render,
  30    SharedString, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext,
  31    VisualContext, WeakView, WindowContext,
  32};
  33use itertools::Itertools;
  34use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
  35use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrev};
  36
  37use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings};
  38use project::{File, Fs, Item, Project};
  39use search::{BufferSearchBar, ProjectSearchView};
  40use serde::{Deserialize, Serialize};
  41use settings::{Settings, SettingsStore};
  42use theme::SyntaxTheme;
  43use util::{RangeExt, ResultExt, TryFutureExt};
  44use workspace::{
  45    dock::{DockPosition, Panel, PanelEvent},
  46    item::ItemHandle,
  47    searchable::{SearchEvent, SearchableItem},
  48    ui::{
  49        h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder,
  50        HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, Label,
  51        LabelCommon, ListItem, Selectable, Spacing, StyledExt, StyledTypography, Tooltip,
  52    },
  53    OpenInTerminal, Workspace,
  54};
  55use worktree::{Entry, ProjectEntryId, WorktreeId};
  56
  57#[derive(Clone, Default, Deserialize, PartialEq)]
  58pub struct Open {
  59    change_selection: bool,
  60}
  61
  62impl_actions!(outline_panel, [Open]);
  63
  64actions!(
  65    outline_panel,
  66    [
  67        CollapseAllEntries,
  68        CollapseSelectedEntry,
  69        CopyPath,
  70        CopyRelativePath,
  71        ExpandAllEntries,
  72        ExpandSelectedEntry,
  73        FoldDirectory,
  74        ToggleActiveEditorPin,
  75        RevealInFileManager,
  76        SelectParent,
  77        ToggleFocus,
  78        UnfoldDirectory,
  79    ]
  80);
  81
  82const OUTLINE_PANEL_KEY: &str = "OutlinePanel";
  83const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
  84
  85type Outline = OutlineItem<language::Anchor>;
  86
  87pub struct OutlinePanel {
  88    fs: Arc<dyn Fs>,
  89    width: Option<Pixels>,
  90    project: Model<Project>,
  91    workspace: View<Workspace>,
  92    active: bool,
  93    pinned: bool,
  94    scroll_handle: UniformListScrollHandle,
  95    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
  96    focus_handle: FocusHandle,
  97    pending_serialization: Task<Option<()>>,
  98    fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,
  99    fs_entries: Vec<FsEntry>,
 100    fs_children_count: HashMap<WorktreeId, HashMap<Arc<Path>, FsChildren>>,
 101    collapsed_entries: HashSet<CollapsedEntry>,
 102    unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
 103    selected_entry: SelectedEntry,
 104    active_item: Option<ActiveItem>,
 105    _subscriptions: Vec<Subscription>,
 106    updating_fs_entries: bool,
 107    fs_entries_update_task: Task<()>,
 108    cached_entries_update_task: Task<()>,
 109    reveal_selection_task: Task<anyhow::Result<()>>,
 110    outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>,
 111    excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
 112    cached_entries: Vec<CachedEntry>,
 113    filter_editor: View<Editor>,
 114    mode: ItemsDisplayMode,
 115    search: Option<(SearchKind, String)>,
 116    search_matches: Vec<Range<editor::Anchor>>,
 117}
 118
 119#[derive(Debug)]
 120enum SelectedEntry {
 121    Invalidated(Option<PanelEntry>),
 122    Valid(PanelEntry),
 123    None,
 124}
 125
 126impl SelectedEntry {
 127    fn invalidate(&mut self) {
 128        match std::mem::replace(self, SelectedEntry::None) {
 129            Self::Valid(entry) => *self = Self::Invalidated(Some(entry)),
 130            Self::None => *self = Self::Invalidated(None),
 131            other => *self = other,
 132        }
 133    }
 134
 135    fn is_invalidated(&self) -> bool {
 136        matches!(self, Self::Invalidated(_))
 137    }
 138}
 139
 140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 141enum ItemsDisplayMode {
 142    Search,
 143    Outline,
 144}
 145
 146#[derive(Debug, Clone, Copy, Default)]
 147struct FsChildren {
 148    files: usize,
 149    dirs: usize,
 150}
 151
 152impl FsChildren {
 153    fn may_be_fold_part(&self) -> bool {
 154        self.dirs == 0 || (self.dirs == 1 && self.files == 0)
 155    }
 156}
 157
 158#[derive(Clone, Debug)]
 159struct CachedEntry {
 160    depth: usize,
 161    string_match: Option<StringMatch>,
 162    entry: PanelEntry,
 163}
 164
 165#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 166enum CollapsedEntry {
 167    Dir(WorktreeId, ProjectEntryId),
 168    File(WorktreeId, BufferId),
 169    ExternalFile(BufferId),
 170    Excerpt(BufferId, ExcerptId),
 171}
 172
 173#[derive(Debug)]
 174struct Excerpt {
 175    range: ExcerptRange<language::Anchor>,
 176    outlines: ExcerptOutlines,
 177}
 178
 179impl Excerpt {
 180    fn invalidate_outlines(&mut self) {
 181        if let ExcerptOutlines::Outlines(valid_outlines) = &mut self.outlines {
 182            self.outlines = ExcerptOutlines::Invalidated(std::mem::take(valid_outlines));
 183        }
 184    }
 185
 186    fn iter_outlines(&self) -> impl Iterator<Item = &Outline> {
 187        match &self.outlines {
 188            ExcerptOutlines::Outlines(outlines) => outlines.iter(),
 189            ExcerptOutlines::Invalidated(outlines) => outlines.iter(),
 190            ExcerptOutlines::NotFetched => [].iter(),
 191        }
 192    }
 193
 194    fn should_fetch_outlines(&self) -> bool {
 195        match &self.outlines {
 196            ExcerptOutlines::Outlines(_) => false,
 197            ExcerptOutlines::Invalidated(_) => true,
 198            ExcerptOutlines::NotFetched => true,
 199        }
 200    }
 201}
 202
 203#[derive(Debug)]
 204enum ExcerptOutlines {
 205    Outlines(Vec<Outline>),
 206    Invalidated(Vec<Outline>),
 207    NotFetched,
 208}
 209
 210#[derive(Clone, Debug)]
 211enum PanelEntry {
 212    Fs(FsEntry),
 213    FoldedDirs(WorktreeId, Vec<Entry>),
 214    Outline(OutlineEntry),
 215    Search(SearchEntry),
 216}
 217
 218#[derive(Clone, Debug)]
 219struct SearchEntry {
 220    match_range: Range<editor::Anchor>,
 221    same_line_matches: Vec<Range<editor::Anchor>>,
 222    kind: SearchKind,
 223    render_data: Option<OnceCell<SearchData>>,
 224}
 225
 226#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 227enum SearchKind {
 228    Project,
 229    Buffer,
 230}
 231
 232#[derive(Clone, Debug)]
 233struct SearchData {
 234    context_range: Range<editor::Anchor>,
 235    context_text: String,
 236    highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
 237    search_match_indices: Vec<Range<usize>>,
 238}
 239
 240impl PartialEq for PanelEntry {
 241    fn eq(&self, other: &Self) -> bool {
 242        match (self, other) {
 243            (Self::Fs(a), Self::Fs(b)) => a == b,
 244            (Self::FoldedDirs(a1, a2), Self::FoldedDirs(b1, b2)) => a1 == b1 && a2 == b2,
 245            (Self::Outline(a), Self::Outline(b)) => a == b,
 246            (
 247                Self::Search(SearchEntry {
 248                    match_range: match_range_a,
 249                    kind: kind_a,
 250                    ..
 251                }),
 252                Self::Search(SearchEntry {
 253                    match_range: match_range_b,
 254                    kind: kind_b,
 255                    ..
 256                }),
 257            ) => match_range_a == match_range_b && kind_a == kind_b,
 258            _ => false,
 259        }
 260    }
 261}
 262
 263impl Eq for PanelEntry {}
 264
 265impl SearchData {
 266    fn new(
 267        kind: SearchKind,
 268        match_range: &Range<editor::Anchor>,
 269        multi_buffer_snapshot: &MultiBufferSnapshot,
 270        theme: &SyntaxTheme,
 271    ) -> Self {
 272        let match_point_range = match_range.to_point(&multi_buffer_snapshot);
 273        let entire_row_range_start = language::Point::new(match_point_range.start.row, 0);
 274        let entire_row_range_end = multi_buffer_snapshot.clip_point(
 275            language::Point::new(match_point_range.end.row, u32::MAX),
 276            Bias::Right,
 277        );
 278        let entire_row_range =
 279            (entire_row_range_start..entire_row_range_end).to_anchors(&multi_buffer_snapshot);
 280        let entire_row_offset_range = entire_row_range.to_offset(&multi_buffer_snapshot);
 281        let match_offset_range = match_range.to_offset(&multi_buffer_snapshot);
 282        let mut search_match_indices = vec![
 283            match_offset_range.start - entire_row_offset_range.start
 284                ..match_offset_range.end - entire_row_offset_range.start,
 285        ];
 286
 287        let mut left_whitespaces_count = 0;
 288        let mut non_whitespace_symbol_occurred = false;
 289        let mut offset = entire_row_offset_range.start;
 290        let mut entire_row_text = String::new();
 291        let mut highlight_ranges = Vec::new();
 292        for mut chunk in multi_buffer_snapshot.chunks(
 293            entire_row_offset_range.start..entire_row_offset_range.end,
 294            true,
 295        ) {
 296            if !non_whitespace_symbol_occurred {
 297                for c in chunk.text.chars() {
 298                    if c.is_whitespace() {
 299                        left_whitespaces_count += 1;
 300                    } else {
 301                        non_whitespace_symbol_occurred = true;
 302                        break;
 303                    }
 304                }
 305            }
 306
 307            if chunk.text.len() > entire_row_offset_range.end - offset {
 308                chunk.text = &chunk.text[0..(entire_row_offset_range.end - offset)];
 309                offset = entire_row_offset_range.end;
 310            } else {
 311                offset += chunk.text.len();
 312            }
 313            let style = chunk
 314                .syntax_highlight_id
 315                .and_then(|highlight| highlight.style(theme));
 316            if let Some(style) = style {
 317                let start = entire_row_text.len();
 318                let end = start + chunk.text.len();
 319                highlight_ranges.push((start..end, style));
 320            }
 321            entire_row_text.push_str(chunk.text);
 322            if offset >= entire_row_offset_range.end {
 323                break;
 324            }
 325        }
 326
 327        if let SearchKind::Buffer = kind {
 328            left_whitespaces_count = 0;
 329        }
 330        highlight_ranges.iter_mut().for_each(|(range, _)| {
 331            range.start = range.start.saturating_sub(left_whitespaces_count);
 332            range.end = range.end.saturating_sub(left_whitespaces_count);
 333        });
 334        search_match_indices.iter_mut().for_each(|range| {
 335            range.start = range.start.saturating_sub(left_whitespaces_count);
 336            range.end = range.end.saturating_sub(left_whitespaces_count);
 337        });
 338        let trimmed_row_offset_range =
 339            entire_row_offset_range.start + left_whitespaces_count..entire_row_offset_range.end;
 340        let trimmed_text = entire_row_text[left_whitespaces_count..].to_owned();
 341        Self {
 342            highlight_ranges,
 343            search_match_indices,
 344            context_range: trimmed_row_offset_range.to_anchors(&multi_buffer_snapshot),
 345            context_text: trimmed_text,
 346        }
 347    }
 348}
 349
 350#[derive(Clone, Debug, PartialEq, Eq)]
 351enum OutlineEntry {
 352    Excerpt(BufferId, ExcerptId, ExcerptRange<language::Anchor>),
 353    Outline(BufferId, ExcerptId, Outline),
 354}
 355
 356#[derive(Clone, Debug, Eq)]
 357enum FsEntry {
 358    ExternalFile(BufferId, Vec<ExcerptId>),
 359    Directory(WorktreeId, Entry),
 360    File(WorktreeId, Entry, BufferId, Vec<ExcerptId>),
 361}
 362
 363impl PartialEq for FsEntry {
 364    fn eq(&self, other: &Self) -> bool {
 365        match (self, other) {
 366            (Self::ExternalFile(id_a, _), Self::ExternalFile(id_b, _)) => id_a == id_b,
 367            (Self::Directory(id_a, entry_a), Self::Directory(id_b, entry_b)) => {
 368                id_a == id_b && entry_a.id == entry_b.id
 369            }
 370            (
 371                Self::File(worktree_a, entry_a, id_a, ..),
 372                Self::File(worktree_b, entry_b, id_b, ..),
 373            ) => worktree_a == worktree_b && entry_a.id == entry_b.id && id_a == id_b,
 374            _ => false,
 375        }
 376    }
 377}
 378
 379struct ActiveItem {
 380    active_editor: WeakView<Editor>,
 381    _buffer_search_subscription: Subscription,
 382    _editor_subscrpiption: Subscription,
 383}
 384
 385#[derive(Debug)]
 386pub enum Event {
 387    Focus,
 388}
 389
 390#[derive(Serialize, Deserialize)]
 391struct SerializedOutlinePanel {
 392    width: Option<Pixels>,
 393    active: Option<bool>,
 394}
 395
 396pub fn init_settings(cx: &mut AppContext) {
 397    OutlinePanelSettings::register(cx);
 398}
 399
 400pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
 401    init_settings(cx);
 402    file_icons::init(assets, cx);
 403
 404    cx.observe_new_views(|workspace: &mut Workspace, _| {
 405        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
 406            workspace.toggle_panel_focus::<OutlinePanel>(cx);
 407        });
 408    })
 409    .detach();
 410}
 411
 412impl OutlinePanel {
 413    pub async fn load(
 414        workspace: WeakView<Workspace>,
 415        mut cx: AsyncWindowContext,
 416    ) -> anyhow::Result<View<Self>> {
 417        let serialized_panel = cx
 418            .background_executor()
 419            .spawn(async move { KEY_VALUE_STORE.read_kvp(OUTLINE_PANEL_KEY) })
 420            .await
 421            .context("loading outline panel")
 422            .log_err()
 423            .flatten()
 424            .map(|panel| serde_json::from_str::<SerializedOutlinePanel>(&panel))
 425            .transpose()
 426            .log_err()
 427            .flatten();
 428
 429        workspace.update(&mut cx, |workspace, cx| {
 430            let panel = Self::new(workspace, cx);
 431            if let Some(serialized_panel) = serialized_panel {
 432                panel.update(cx, |panel, cx| {
 433                    panel.width = serialized_panel.width.map(|px| px.round());
 434                    panel.active = serialized_panel.active.unwrap_or(false);
 435                    cx.notify();
 436                });
 437            }
 438            panel
 439        })
 440    }
 441
 442    fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 443        let project = workspace.project().clone();
 444        let workspace_handle = cx.view().clone();
 445        let outline_panel = cx.new_view(|cx| {
 446            let filter_editor = cx.new_view(|cx| {
 447                let mut editor = Editor::single_line(cx);
 448                editor.set_placeholder_text("Filter...", cx);
 449                editor
 450            });
 451            let filter_update_subscription =
 452                cx.subscribe(&filter_editor, |outline_panel: &mut Self, _, event, cx| {
 453                    if let editor::EditorEvent::BufferEdited = event {
 454                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
 455                    }
 456                });
 457
 458            let focus_handle = cx.focus_handle();
 459            let focus_subscription = cx.on_focus(&focus_handle, Self::focus_in);
 460            let workspace_subscription = cx.subscribe(
 461                &workspace
 462                    .weak_handle()
 463                    .upgrade()
 464                    .expect("have a &mut Workspace"),
 465                move |outline_panel, workspace, event, cx| {
 466                    if let workspace::Event::ActiveItemChanged = event {
 467                        if let Some(new_active_editor) =
 468                            workspace_active_editor(workspace.read(cx), cx)
 469                        {
 470                            if outline_panel.should_replace_active_editor(&new_active_editor) {
 471                                outline_panel.replace_active_editor(new_active_editor, cx);
 472                            }
 473                        } else {
 474                            outline_panel.clear_previous(cx);
 475                            cx.notify();
 476                        }
 477                    }
 478                },
 479            );
 480
 481            let icons_subscription = cx.observe_global::<FileIcons>(|_, cx| {
 482                cx.notify();
 483            });
 484
 485            let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx);
 486            let settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
 487                let new_settings = *OutlinePanelSettings::get_global(cx);
 488                if outline_panel_settings != new_settings {
 489                    outline_panel_settings = new_settings;
 490                    cx.notify();
 491                }
 492            });
 493
 494            let mut outline_panel = Self {
 495                mode: ItemsDisplayMode::Outline,
 496                active: false,
 497                pinned: false,
 498                workspace: workspace_handle,
 499                project,
 500                fs: workspace.app_state().fs.clone(),
 501                scroll_handle: UniformListScrollHandle::new(),
 502                focus_handle,
 503                filter_editor,
 504                fs_entries: Vec::new(),
 505                search_matches: Vec::new(),
 506                search: None,
 507                fs_entries_depth: HashMap::default(),
 508                fs_children_count: HashMap::default(),
 509                collapsed_entries: HashSet::default(),
 510                unfolded_dirs: HashMap::default(),
 511                selected_entry: SelectedEntry::None,
 512                context_menu: None,
 513                width: None,
 514                active_item: None,
 515                pending_serialization: Task::ready(None),
 516                updating_fs_entries: false,
 517                fs_entries_update_task: Task::ready(()),
 518                cached_entries_update_task: Task::ready(()),
 519                reveal_selection_task: Task::ready(Ok(())),
 520                outline_fetch_tasks: HashMap::default(),
 521                excerpts: HashMap::default(),
 522                cached_entries: Vec::new(),
 523                _subscriptions: vec![
 524                    settings_subscription,
 525                    icons_subscription,
 526                    focus_subscription,
 527                    workspace_subscription,
 528                    filter_update_subscription,
 529                ],
 530            };
 531            if let Some(editor) = workspace_active_editor(workspace, cx) {
 532                outline_panel.replace_active_editor(editor, cx);
 533            }
 534            outline_panel
 535        });
 536
 537        outline_panel
 538    }
 539
 540    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 541        let width = self.width;
 542        let active = Some(self.active);
 543        self.pending_serialization = cx.background_executor().spawn(
 544            async move {
 545                KEY_VALUE_STORE
 546                    .write_kvp(
 547                        OUTLINE_PANEL_KEY.into(),
 548                        serde_json::to_string(&SerializedOutlinePanel { width, active })?,
 549                    )
 550                    .await?;
 551                anyhow::Ok(())
 552            }
 553            .log_err(),
 554        );
 555    }
 556
 557    fn dispatch_context(&self, _: &ViewContext<Self>) -> KeyContext {
 558        let mut dispatch_context = KeyContext::new_with_defaults();
 559        dispatch_context.add("OutlinePanel");
 560        dispatch_context.add("menu");
 561        dispatch_context
 562    }
 563
 564    fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext<Self>) {
 565        if let Some(PanelEntry::FoldedDirs(worktree_id, entries)) = self.selected_entry().cloned() {
 566            self.unfolded_dirs
 567                .entry(worktree_id)
 568                .or_default()
 569                .extend(entries.iter().map(|entry| entry.id));
 570            self.update_cached_entries(None, cx);
 571        }
 572    }
 573
 574    fn fold_directory(&mut self, _: &FoldDirectory, cx: &mut ViewContext<Self>) {
 575        let (worktree_id, entry) = match self.selected_entry().cloned() {
 576            Some(PanelEntry::Fs(FsEntry::Directory(worktree_id, entry))) => {
 577                (worktree_id, Some(entry))
 578            }
 579            Some(PanelEntry::FoldedDirs(worktree_id, entries)) => {
 580                (worktree_id, entries.last().cloned())
 581            }
 582            _ => return,
 583        };
 584        let Some(entry) = entry else {
 585            return;
 586        };
 587        let unfolded_dirs = self.unfolded_dirs.get_mut(&worktree_id);
 588        let worktree = self
 589            .project
 590            .read(cx)
 591            .worktree_for_id(worktree_id, cx)
 592            .map(|w| w.read(cx).snapshot());
 593        let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
 594            return;
 595        };
 596
 597        unfolded_dirs.remove(&entry.id);
 598        self.update_cached_entries(None, cx);
 599    }
 600
 601    fn open(&mut self, open: &Open, cx: &mut ViewContext<Self>) {
 602        if self.filter_editor.focus_handle(cx).is_focused(cx) {
 603            cx.propagate()
 604        } else if let Some(selected_entry) = self.selected_entry().cloned() {
 605            self.open_entry(&selected_entry, open.change_selection, cx);
 606        }
 607    }
 608
 609    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
 610        if self.filter_editor.focus_handle(cx).is_focused(cx) {
 611            self.focus_handle.focus(cx);
 612        } else {
 613            self.filter_editor.focus_handle(cx).focus(cx);
 614        }
 615
 616        if self.context_menu.is_some() {
 617            self.context_menu.take();
 618            cx.notify();
 619        }
 620    }
 621
 622    fn open_entry(
 623        &mut self,
 624        entry: &PanelEntry,
 625        change_selection: bool,
 626        cx: &mut ViewContext<OutlinePanel>,
 627    ) {
 628        let Some(active_editor) = self.active_editor() else {
 629            return;
 630        };
 631        let active_multi_buffer = active_editor.read(cx).buffer().clone();
 632        let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
 633        let offset_from_top = if active_multi_buffer.read(cx).is_singleton() {
 634            Point::default()
 635        } else {
 636            Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32))
 637        };
 638
 639        self.toggle_expanded(entry, cx);
 640        let scroll_target = match entry {
 641            PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => None,
 642            PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
 643                let scroll_target = multi_buffer_snapshot.excerpts().find_map(
 644                    |(excerpt_id, buffer_snapshot, excerpt_range)| {
 645                        if &buffer_snapshot.remote_id() == buffer_id {
 646                            multi_buffer_snapshot
 647                                .anchor_in_excerpt(excerpt_id, excerpt_range.context.start)
 648                        } else {
 649                            None
 650                        }
 651                    },
 652                );
 653                Some(offset_from_top).zip(scroll_target)
 654            }
 655            PanelEntry::Fs(FsEntry::File(_, file_entry, ..)) => {
 656                let scroll_target = self
 657                    .project
 658                    .update(cx, |project, cx| {
 659                        project
 660                            .path_for_entry(file_entry.id, cx)
 661                            .and_then(|path| project.get_open_buffer(&path, cx))
 662                    })
 663                    .map(|buffer| {
 664                        active_multi_buffer
 665                            .read(cx)
 666                            .excerpts_for_buffer(&buffer, cx)
 667                    })
 668                    .and_then(|excerpts| {
 669                        let (excerpt_id, excerpt_range) = excerpts.first()?;
 670                        multi_buffer_snapshot
 671                            .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
 672                    });
 673                Some(offset_from_top).zip(scroll_target)
 674            }
 675            PanelEntry::Outline(OutlineEntry::Outline(_, excerpt_id, outline)) => {
 676                let scroll_target = multi_buffer_snapshot
 677                    .anchor_in_excerpt(*excerpt_id, outline.range.start)
 678                    .or_else(|| {
 679                        multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, outline.range.end)
 680                    });
 681                Some(Point::default()).zip(scroll_target)
 682            }
 683            PanelEntry::Outline(OutlineEntry::Excerpt(_, excerpt_id, excerpt_range)) => {
 684                let scroll_target = multi_buffer_snapshot
 685                    .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start);
 686                Some(Point::default()).zip(scroll_target)
 687            }
 688            PanelEntry::Search(SearchEntry { match_range, .. }) => {
 689                Some((Point::default(), match_range.start))
 690            }
 691        };
 692
 693        if let Some((offset, anchor)) = scroll_target {
 694            self.select_entry(entry.clone(), true, cx);
 695            if change_selection {
 696                active_editor.update(cx, |editor, cx| {
 697                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 698                        s.select_ranges(Some(anchor..anchor))
 699                    });
 700                });
 701                active_editor.focus_handle(cx).focus(cx);
 702            } else {
 703                active_editor.update(cx, |editor, cx| {
 704                    editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, cx);
 705                });
 706                self.focus_handle.focus(cx);
 707            }
 708
 709            if let PanelEntry::Search(_) = entry {
 710                if let Some(active_project_search) =
 711                    self.active_project_search(Some(&active_editor), cx)
 712                {
 713                    self.workspace.update(cx, |workspace, cx| {
 714                        workspace.activate_item(&active_project_search, true, change_selection, cx)
 715                    });
 716                }
 717            } else {
 718                self.workspace.update(cx, |workspace, cx| {
 719                    workspace.activate_item(&active_editor, true, change_selection, cx)
 720                });
 721            };
 722        }
 723    }
 724
 725    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
 726        if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
 727            self.cached_entries
 728                .iter()
 729                .map(|cached_entry| &cached_entry.entry)
 730                .skip_while(|entry| entry != &selected_entry)
 731                .skip(1)
 732                .next()
 733                .cloned()
 734        }) {
 735            self.select_entry(entry_to_select, true, cx);
 736        } else {
 737            self.select_first(&SelectFirst {}, cx)
 738        }
 739    }
 740
 741    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
 742        if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
 743            self.cached_entries
 744                .iter()
 745                .rev()
 746                .map(|cached_entry| &cached_entry.entry)
 747                .skip_while(|entry| entry != &selected_entry)
 748                .skip(1)
 749                .next()
 750                .cloned()
 751        }) {
 752            self.select_entry(entry_to_select, true, cx);
 753        } else {
 754            self.select_first(&SelectFirst {}, cx)
 755        }
 756    }
 757
 758    fn select_parent(&mut self, _: &SelectParent, cx: &mut ViewContext<Self>) {
 759        if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
 760            let mut previous_entries = self
 761                .cached_entries
 762                .iter()
 763                .rev()
 764                .map(|cached_entry| &cached_entry.entry)
 765                .skip_while(|entry| entry != &selected_entry)
 766                .skip(1);
 767            match &selected_entry {
 768                PanelEntry::Fs(fs_entry) => match fs_entry {
 769                    FsEntry::ExternalFile(..) => None,
 770                    FsEntry::File(worktree_id, entry, ..)
 771                    | FsEntry::Directory(worktree_id, entry) => {
 772                        entry.path.parent().and_then(|parent_path| {
 773                            previous_entries.find(|entry| match entry {
 774                                PanelEntry::Fs(FsEntry::Directory(dir_worktree_id, dir_entry)) => {
 775                                    dir_worktree_id == worktree_id
 776                                        && dir_entry.path.as_ref() == parent_path
 777                                }
 778                                PanelEntry::FoldedDirs(dirs_worktree_id, dirs) => {
 779                                    dirs_worktree_id == worktree_id
 780                                        && dirs
 781                                            .first()
 782                                            .map_or(false, |dir| dir.path.as_ref() == parent_path)
 783                                }
 784                                _ => false,
 785                            })
 786                        })
 787                    }
 788                },
 789                PanelEntry::FoldedDirs(worktree_id, entries) => entries
 790                    .first()
 791                    .and_then(|entry| entry.path.parent())
 792                    .and_then(|parent_path| {
 793                        previous_entries.find(|entry| {
 794                            if let PanelEntry::Fs(FsEntry::Directory(dir_worktree_id, dir_entry)) =
 795                                entry
 796                            {
 797                                dir_worktree_id == worktree_id
 798                                    && dir_entry.path.as_ref() == parent_path
 799                            } else {
 800                                false
 801                            }
 802                        })
 803                    }),
 804                PanelEntry::Outline(OutlineEntry::Excerpt(excerpt_buffer_id, excerpt_id, _)) => {
 805                    previous_entries.find(|entry| match entry {
 806                        PanelEntry::Fs(FsEntry::File(_, _, file_buffer_id, file_excerpts)) => {
 807                            file_buffer_id == excerpt_buffer_id
 808                                && file_excerpts.contains(&excerpt_id)
 809                        }
 810                        PanelEntry::Fs(FsEntry::ExternalFile(file_buffer_id, file_excerpts)) => {
 811                            file_buffer_id == excerpt_buffer_id
 812                                && file_excerpts.contains(&excerpt_id)
 813                        }
 814                        _ => false,
 815                    })
 816                }
 817                PanelEntry::Outline(OutlineEntry::Outline(
 818                    outline_buffer_id,
 819                    outline_excerpt_id,
 820                    _,
 821                )) => previous_entries.find(|entry| {
 822                    if let PanelEntry::Outline(OutlineEntry::Excerpt(
 823                        excerpt_buffer_id,
 824                        excerpt_id,
 825                        _,
 826                    )) = entry
 827                    {
 828                        outline_buffer_id == excerpt_buffer_id && outline_excerpt_id == excerpt_id
 829                    } else {
 830                        false
 831                    }
 832                }),
 833                PanelEntry::Search(_) => {
 834                    previous_entries.find(|entry| !matches!(entry, PanelEntry::Search(_)))
 835                }
 836            }
 837        }) {
 838            self.select_entry(entry_to_select.clone(), true, cx);
 839        } else {
 840            self.select_first(&SelectFirst {}, cx);
 841        }
 842    }
 843
 844    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
 845        if let Some(first_entry) = self.cached_entries.iter().next() {
 846            self.select_entry(first_entry.entry.clone(), true, cx);
 847        }
 848    }
 849
 850    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
 851        if let Some(new_selection) = self
 852            .cached_entries
 853            .iter()
 854            .rev()
 855            .map(|cached_entry| &cached_entry.entry)
 856            .next()
 857        {
 858            self.select_entry(new_selection.clone(), true, cx);
 859        }
 860    }
 861
 862    fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
 863        if let Some(selected_entry) = self.selected_entry() {
 864            let index = self
 865                .cached_entries
 866                .iter()
 867                .position(|cached_entry| &cached_entry.entry == selected_entry);
 868            if let Some(index) = index {
 869                self.scroll_handle.scroll_to_item(index);
 870                cx.notify();
 871            }
 872        }
 873    }
 874
 875    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 876        if !self.focus_handle.contains_focused(cx) {
 877            cx.emit(Event::Focus);
 878        }
 879    }
 880
 881    fn deploy_context_menu(
 882        &mut self,
 883        position: Point<Pixels>,
 884        entry: PanelEntry,
 885        cx: &mut ViewContext<Self>,
 886    ) {
 887        self.select_entry(entry.clone(), true, cx);
 888        let is_root = match &entry {
 889            PanelEntry::Fs(FsEntry::File(worktree_id, entry, ..))
 890            | PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => self
 891                .project
 892                .read(cx)
 893                .worktree_for_id(*worktree_id, cx)
 894                .map(|worktree| {
 895                    worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
 896                })
 897                .unwrap_or(false),
 898            PanelEntry::FoldedDirs(worktree_id, entries) => entries
 899                .first()
 900                .and_then(|entry| {
 901                    self.project
 902                        .read(cx)
 903                        .worktree_for_id(*worktree_id, cx)
 904                        .map(|worktree| {
 905                            worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
 906                        })
 907                })
 908                .unwrap_or(false),
 909            PanelEntry::Fs(FsEntry::ExternalFile(..)) => false,
 910            PanelEntry::Outline(..) => {
 911                cx.notify();
 912                return;
 913            }
 914            PanelEntry::Search(_) => {
 915                cx.notify();
 916                return;
 917            }
 918        };
 919        let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
 920        let is_foldable = auto_fold_dirs && !is_root && self.is_foldable(&entry);
 921        let is_unfoldable = auto_fold_dirs && !is_root && self.is_unfoldable(&entry);
 922
 923        let context_menu = ContextMenu::build(cx, |menu, _| {
 924            menu.context(self.focus_handle.clone())
 925                .when(cfg!(target_os = "macos"), |menu| {
 926                    menu.action("Reveal in Finder", Box::new(RevealInFileManager))
 927                })
 928                .when(cfg!(not(target_os = "macos")), |menu| {
 929                    menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
 930                })
 931                .action("Open in Terminal", Box::new(OpenInTerminal))
 932                .when(is_unfoldable, |menu| {
 933                    menu.action("Unfold Directory", Box::new(UnfoldDirectory))
 934                })
 935                .when(is_foldable, |menu| {
 936                    menu.action("Fold Directory", Box::new(FoldDirectory))
 937                })
 938                .separator()
 939                .action("Copy Path", Box::new(CopyPath))
 940                .action("Copy Relative Path", Box::new(CopyRelativePath))
 941        });
 942        cx.focus_view(&context_menu);
 943        let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| {
 944            outline_panel.context_menu.take();
 945            cx.notify();
 946        });
 947        self.context_menu = Some((context_menu, position, subscription));
 948        cx.notify();
 949    }
 950
 951    fn is_unfoldable(&self, entry: &PanelEntry) -> bool {
 952        matches!(entry, PanelEntry::FoldedDirs(..))
 953    }
 954
 955    fn is_foldable(&self, entry: &PanelEntry) -> bool {
 956        let (directory_worktree, directory_entry) = match entry {
 957            PanelEntry::Fs(FsEntry::Directory(directory_worktree, directory_entry)) => {
 958                (*directory_worktree, Some(directory_entry))
 959            }
 960            _ => return false,
 961        };
 962        let Some(directory_entry) = directory_entry else {
 963            return false;
 964        };
 965
 966        if self
 967            .unfolded_dirs
 968            .get(&directory_worktree)
 969            .map_or(true, |unfolded_dirs| {
 970                !unfolded_dirs.contains(&directory_entry.id)
 971            })
 972        {
 973            return false;
 974        }
 975
 976        let children = self
 977            .fs_children_count
 978            .get(&directory_worktree)
 979            .and_then(|entries| entries.get(&directory_entry.path))
 980            .copied()
 981            .unwrap_or_default();
 982
 983        children.may_be_fold_part() && children.dirs > 0
 984    }
 985
 986    fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
 987        let entry_to_expand = match self.selected_entry() {
 988            Some(PanelEntry::FoldedDirs(worktree_id, dir_entries)) => dir_entries
 989                .last()
 990                .map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id)),
 991            Some(PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry))) => {
 992                Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
 993            }
 994            Some(PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _))) => {
 995                Some(CollapsedEntry::File(*worktree_id, *buffer_id))
 996            }
 997            Some(PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _))) => {
 998                Some(CollapsedEntry::ExternalFile(*buffer_id))
 999            }
1000            Some(PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _))) => {
1001                Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
1002            }
1003            None | Some(PanelEntry::Search(_)) | Some(PanelEntry::Outline(..)) => None,
1004        };
1005        let Some(collapsed_entry) = entry_to_expand else {
1006            return;
1007        };
1008        let expanded = self.collapsed_entries.remove(&collapsed_entry);
1009        if expanded {
1010            if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
1011                self.project.update(cx, |project, cx| {
1012                    project.expand_entry(worktree_id, dir_entry_id, cx);
1013                });
1014            }
1015            self.update_cached_entries(None, cx);
1016        } else {
1017            self.select_next(&SelectNext, cx)
1018        }
1019    }
1020
1021    fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
1022        let Some(selected_entry) = self.selected_entry().cloned() else {
1023            return;
1024        };
1025        match &selected_entry {
1026            PanelEntry::Fs(FsEntry::Directory(worktree_id, selected_dir_entry)) => {
1027                self.collapsed_entries
1028                    .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id));
1029                self.select_entry(selected_entry, true, cx);
1030                self.update_cached_entries(None, cx);
1031            }
1032            PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1033                self.collapsed_entries
1034                    .insert(CollapsedEntry::File(*worktree_id, *buffer_id));
1035                self.select_entry(selected_entry, true, cx);
1036                self.update_cached_entries(None, cx);
1037            }
1038            PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
1039                self.collapsed_entries
1040                    .insert(CollapsedEntry::ExternalFile(*buffer_id));
1041                self.select_entry(selected_entry, true, cx);
1042                self.update_cached_entries(None, cx);
1043            }
1044            PanelEntry::FoldedDirs(worktree_id, dir_entries) => {
1045                if let Some(dir_entry) = dir_entries.last() {
1046                    if self
1047                        .collapsed_entries
1048                        .insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
1049                    {
1050                        self.select_entry(selected_entry, true, cx);
1051                        self.update_cached_entries(None, cx);
1052                    }
1053                }
1054            }
1055            PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
1056                if self
1057                    .collapsed_entries
1058                    .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
1059                {
1060                    self.select_entry(selected_entry, true, cx);
1061                    self.update_cached_entries(None, cx);
1062                }
1063            }
1064            PanelEntry::Search(_) | PanelEntry::Outline(..) => {}
1065        }
1066    }
1067
1068    pub fn expand_all_entries(&mut self, _: &ExpandAllEntries, cx: &mut ViewContext<Self>) {
1069        let expanded_entries =
1070            self.fs_entries
1071                .iter()
1072                .fold(HashSet::default(), |mut entries, fs_entry| {
1073                    match fs_entry {
1074                        FsEntry::ExternalFile(buffer_id, _) => {
1075                            entries.insert(CollapsedEntry::ExternalFile(*buffer_id));
1076                            entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
1077                                |excerpts| {
1078                                    excerpts.iter().map(|(excerpt_id, _)| {
1079                                        CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)
1080                                    })
1081                                },
1082                            ));
1083                        }
1084                        FsEntry::Directory(worktree_id, entry) => {
1085                            entries.insert(CollapsedEntry::Dir(*worktree_id, entry.id));
1086                        }
1087                        FsEntry::File(worktree_id, _, buffer_id, _) => {
1088                            entries.insert(CollapsedEntry::File(*worktree_id, *buffer_id));
1089                            entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
1090                                |excerpts| {
1091                                    excerpts.iter().map(|(excerpt_id, _)| {
1092                                        CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)
1093                                    })
1094                                },
1095                            ));
1096                        }
1097                    }
1098                    entries
1099                });
1100        self.collapsed_entries
1101            .retain(|entry| !expanded_entries.contains(entry));
1102        self.update_cached_entries(None, cx);
1103    }
1104
1105    pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
1106        let new_entries = self
1107            .cached_entries
1108            .iter()
1109            .flat_map(|cached_entry| match &cached_entry.entry {
1110                PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => {
1111                    Some(CollapsedEntry::Dir(*worktree_id, entry.id))
1112                }
1113                PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1114                    Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1115                }
1116                PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
1117                    Some(CollapsedEntry::ExternalFile(*buffer_id))
1118                }
1119                PanelEntry::FoldedDirs(worktree_id, entries) => {
1120                    Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id))
1121                }
1122                PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
1123                    Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
1124                }
1125                PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1126            })
1127            .collect::<Vec<_>>();
1128        self.collapsed_entries.extend(new_entries);
1129        self.update_cached_entries(None, cx);
1130    }
1131
1132    fn toggle_expanded(&mut self, entry: &PanelEntry, cx: &mut ViewContext<Self>) {
1133        match entry {
1134            PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry)) => {
1135                let entry_id = dir_entry.id;
1136                let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1137                if self.collapsed_entries.remove(&collapsed_entry) {
1138                    self.project
1139                        .update(cx, |project, cx| {
1140                            project.expand_entry(*worktree_id, entry_id, cx)
1141                        })
1142                        .unwrap_or_else(|| Task::ready(Ok(())))
1143                        .detach_and_log_err(cx);
1144                } else {
1145                    self.collapsed_entries.insert(collapsed_entry);
1146                }
1147            }
1148            PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1149                let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
1150                if !self.collapsed_entries.remove(&collapsed_entry) {
1151                    self.collapsed_entries.insert(collapsed_entry);
1152                }
1153            }
1154            PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
1155                let collapsed_entry = CollapsedEntry::ExternalFile(*buffer_id);
1156                if !self.collapsed_entries.remove(&collapsed_entry) {
1157                    self.collapsed_entries.insert(collapsed_entry);
1158                }
1159            }
1160            PanelEntry::FoldedDirs(worktree_id, dir_entries) => {
1161                if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) {
1162                    let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1163                    if self.collapsed_entries.remove(&collapsed_entry) {
1164                        self.project
1165                            .update(cx, |project, cx| {
1166                                project.expand_entry(*worktree_id, entry_id, cx)
1167                            })
1168                            .unwrap_or_else(|| Task::ready(Ok(())))
1169                            .detach_and_log_err(cx);
1170                    } else {
1171                        self.collapsed_entries.insert(collapsed_entry);
1172                    }
1173                }
1174            }
1175            PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
1176                let collapsed_entry = CollapsedEntry::Excerpt(*buffer_id, *excerpt_id);
1177                if !self.collapsed_entries.remove(&collapsed_entry) {
1178                    self.collapsed_entries.insert(collapsed_entry);
1179                }
1180            }
1181            PanelEntry::Search(_) | PanelEntry::Outline(..) => return,
1182        }
1183
1184        self.select_entry(entry.clone(), true, cx);
1185        self.update_cached_entries(None, cx);
1186    }
1187
1188    fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
1189        if let Some(clipboard_text) = self
1190            .selected_entry()
1191            .and_then(|entry| self.abs_path(&entry, cx))
1192            .map(|p| p.to_string_lossy().to_string())
1193        {
1194            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1195        }
1196    }
1197
1198    fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
1199        if let Some(clipboard_text) = self
1200            .selected_entry()
1201            .and_then(|entry| match entry {
1202                PanelEntry::Fs(entry) => self.relative_path(&entry, cx),
1203                PanelEntry::FoldedDirs(_, dirs) => dirs.last().map(|entry| entry.path.clone()),
1204                PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1205            })
1206            .map(|p| p.to_string_lossy().to_string())
1207        {
1208            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1209        }
1210    }
1211
1212    fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
1213        if let Some(abs_path) = self
1214            .selected_entry()
1215            .and_then(|entry| self.abs_path(&entry, cx))
1216        {
1217            cx.reveal_path(&abs_path);
1218        }
1219    }
1220
1221    fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
1222        let selected_entry = self.selected_entry();
1223        let abs_path = selected_entry.and_then(|entry| self.abs_path(&entry, cx));
1224        let working_directory = if let (
1225            Some(abs_path),
1226            Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))),
1227        ) = (&abs_path, selected_entry)
1228        {
1229            abs_path.parent().map(|p| p.to_owned())
1230        } else {
1231            abs_path
1232        };
1233
1234        if let Some(working_directory) = working_directory {
1235            cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone())
1236        }
1237    }
1238
1239    fn reveal_entry_for_selection(
1240        &mut self,
1241        editor: &View<Editor>,
1242        cx: &mut ViewContext<'_, Self>,
1243    ) {
1244        if !self.active {
1245            return;
1246        }
1247        if !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
1248            return;
1249        }
1250        let Some(entry_with_selection) = self.location_for_editor_selection(editor, cx) else {
1251            self.selected_entry = SelectedEntry::None;
1252            cx.notify();
1253            return;
1254        };
1255
1256        let project = self.project.clone();
1257        self.reveal_selection_task = cx.spawn(|outline_panel, mut cx| async move {
1258            cx.background_executor().timer(UPDATE_DEBOUNCE).await;
1259            let related_buffer_entry = match &entry_with_selection {
1260                PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1261                    project.update(&mut cx, |project, cx| {
1262                        let entry_id = project
1263                            .buffer_for_id(*buffer_id, cx)
1264                            .and_then(|buffer| buffer.read(cx).entry_id(cx));
1265                        project
1266                            .worktree_for_id(*worktree_id, cx)
1267                            .zip(entry_id)
1268                            .and_then(|(worktree, entry_id)| {
1269                                let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1270                                Some((worktree, entry))
1271                            })
1272                    })?
1273                }
1274                PanelEntry::Outline(outline_entry) => {
1275                    let &(OutlineEntry::Outline(buffer_id, excerpt_id, _)
1276                    | OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) = outline_entry;
1277                    outline_panel.update(&mut cx, |outline_panel, cx| {
1278                        outline_panel
1279                            .collapsed_entries
1280                            .remove(&CollapsedEntry::ExternalFile(buffer_id));
1281                        outline_panel
1282                            .collapsed_entries
1283                            .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1284                        let project = outline_panel.project.read(cx);
1285                        let entry_id = project
1286                            .buffer_for_id(buffer_id, cx)
1287                            .and_then(|buffer| buffer.read(cx).entry_id(cx));
1288
1289                        entry_id.and_then(|entry_id| {
1290                            project
1291                                .worktree_for_entry(entry_id, cx)
1292                                .and_then(|worktree| {
1293                                    let worktree_id = worktree.read(cx).id();
1294                                    outline_panel
1295                                        .collapsed_entries
1296                                        .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1297                                    let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1298                                    Some((worktree, entry))
1299                                })
1300                        })
1301                    })?
1302                }
1303                PanelEntry::Fs(FsEntry::ExternalFile(..)) => None,
1304                PanelEntry::Search(SearchEntry { match_range, .. }) => match_range
1305                    .start
1306                    .buffer_id
1307                    .or(match_range.end.buffer_id)
1308                    .map(|buffer_id| {
1309                        outline_panel.update(&mut cx, |outline_panel, cx| {
1310                            outline_panel
1311                                .collapsed_entries
1312                                .remove(&CollapsedEntry::ExternalFile(buffer_id));
1313                            let project = project.read(cx);
1314                            let entry_id = project
1315                                .buffer_for_id(buffer_id, cx)
1316                                .and_then(|buffer| buffer.read(cx).entry_id(cx));
1317
1318                            entry_id.and_then(|entry_id| {
1319                                project
1320                                    .worktree_for_entry(entry_id, cx)
1321                                    .and_then(|worktree| {
1322                                        let worktree_id = worktree.read(cx).id();
1323                                        outline_panel
1324                                            .collapsed_entries
1325                                            .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1326                                        let entry =
1327                                            worktree.read(cx).entry_for_id(entry_id)?.clone();
1328                                        Some((worktree, entry))
1329                                    })
1330                            })
1331                        })
1332                    })
1333                    .transpose()?
1334                    .flatten(),
1335                _ => return anyhow::Ok(()),
1336            };
1337            if let Some((worktree, buffer_entry)) = related_buffer_entry {
1338                outline_panel.update(&mut cx, |outline_panel, cx| {
1339                    let worktree_id = worktree.read(cx).id();
1340                    let mut dirs_to_expand = Vec::new();
1341                    {
1342                        let mut traversal = worktree.read(cx).traverse_from_path(
1343                            true,
1344                            true,
1345                            true,
1346                            buffer_entry.path.as_ref(),
1347                        );
1348                        let mut current_entry = buffer_entry;
1349                        loop {
1350                            if current_entry.is_dir() {
1351                                if outline_panel
1352                                    .collapsed_entries
1353                                    .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
1354                                {
1355                                    dirs_to_expand.push(current_entry.id);
1356                                }
1357                            }
1358
1359                            if traversal.back_to_parent() {
1360                                if let Some(parent_entry) = traversal.entry() {
1361                                    current_entry = parent_entry.clone();
1362                                    continue;
1363                                }
1364                            }
1365                            break;
1366                        }
1367                    }
1368                    for dir_to_expand in dirs_to_expand {
1369                        project
1370                            .update(cx, |project, cx| {
1371                                project.expand_entry(worktree_id, dir_to_expand, cx)
1372                            })
1373                            .unwrap_or_else(|| Task::ready(Ok(())))
1374                            .detach_and_log_err(cx)
1375                    }
1376                })?
1377            }
1378
1379            outline_panel.update(&mut cx, |outline_panel, cx| {
1380                outline_panel.select_entry(entry_with_selection, false, cx);
1381                outline_panel.update_cached_entries(None, cx);
1382            })?;
1383
1384            anyhow::Ok(())
1385        });
1386    }
1387
1388    fn render_excerpt(
1389        &self,
1390        buffer_id: BufferId,
1391        excerpt_id: ExcerptId,
1392        range: &ExcerptRange<language::Anchor>,
1393        depth: usize,
1394        cx: &mut ViewContext<OutlinePanel>,
1395    ) -> Option<Stateful<Div>> {
1396        let item_id = ElementId::from(excerpt_id.to_proto() as usize);
1397        let is_active = match self.selected_entry() {
1398            Some(PanelEntry::Outline(OutlineEntry::Excerpt(
1399                selected_buffer_id,
1400                selected_excerpt_id,
1401                _,
1402            ))) => selected_buffer_id == &buffer_id && selected_excerpt_id == &excerpt_id,
1403            _ => false,
1404        };
1405        let has_outlines = self
1406            .excerpts
1407            .get(&buffer_id)
1408            .and_then(|excerpts| match &excerpts.get(&excerpt_id)?.outlines {
1409                ExcerptOutlines::Outlines(outlines) => Some(outlines),
1410                ExcerptOutlines::Invalidated(outlines) => Some(outlines),
1411                ExcerptOutlines::NotFetched => None,
1412            })
1413            .map_or(false, |outlines| !outlines.is_empty());
1414        let is_expanded = !self
1415            .collapsed_entries
1416            .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1417        let color = entry_git_aware_label_color(None, false, is_active);
1418        let icon = if has_outlines {
1419            FileIcons::get_chevron_icon(is_expanded, cx)
1420                .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
1421        } else {
1422            None
1423        }
1424        .unwrap_or_else(empty_icon);
1425
1426        let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
1427        let excerpt_range = range.context.to_point(&buffer_snapshot);
1428        let label_element = Label::new(format!(
1429            "Lines {}- {}",
1430            excerpt_range.start.row + 1,
1431            excerpt_range.end.row + 1,
1432        ))
1433        .single_line()
1434        .color(color)
1435        .into_any_element();
1436
1437        Some(self.entry_element(
1438            PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, range.clone())),
1439            item_id,
1440            depth,
1441            Some(icon),
1442            is_active,
1443            label_element,
1444            cx,
1445        ))
1446    }
1447
1448    fn render_outline(
1449        &self,
1450        buffer_id: BufferId,
1451        excerpt_id: ExcerptId,
1452        rendered_outline: &Outline,
1453        depth: usize,
1454        string_match: Option<&StringMatch>,
1455        cx: &mut ViewContext<Self>,
1456    ) -> Stateful<Div> {
1457        let (item_id, label_element) = (
1458            ElementId::from(SharedString::from(format!(
1459                "{buffer_id:?}|{excerpt_id:?}{:?}|{:?}",
1460                rendered_outline.range, &rendered_outline.text,
1461            ))),
1462            language::render_item(
1463                &rendered_outline,
1464                string_match
1465                    .map(|string_match| string_match.ranges().collect::<Vec<_>>())
1466                    .unwrap_or_default(),
1467                cx,
1468            )
1469            .into_any_element(),
1470        );
1471        let is_active = match self.selected_entry() {
1472            Some(PanelEntry::Outline(OutlineEntry::Outline(
1473                selected_buffer_id,
1474                selected_excerpt_id,
1475                selected_entry,
1476            ))) => {
1477                selected_buffer_id == &buffer_id
1478                    && selected_excerpt_id == &excerpt_id
1479                    && selected_entry == rendered_outline
1480            }
1481            _ => false,
1482        };
1483        let icon = if self.is_singleton_active(cx) {
1484            None
1485        } else {
1486            Some(empty_icon())
1487        };
1488        self.entry_element(
1489            PanelEntry::Outline(OutlineEntry::Outline(
1490                buffer_id,
1491                excerpt_id,
1492                rendered_outline.clone(),
1493            )),
1494            item_id,
1495            depth,
1496            icon,
1497            is_active,
1498            label_element,
1499            cx,
1500        )
1501    }
1502
1503    fn render_entry(
1504        &self,
1505        rendered_entry: &FsEntry,
1506        depth: usize,
1507        string_match: Option<&StringMatch>,
1508        cx: &mut ViewContext<Self>,
1509    ) -> Stateful<Div> {
1510        let settings = OutlinePanelSettings::get_global(cx);
1511        let is_active = match self.selected_entry() {
1512            Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry,
1513            _ => false,
1514        };
1515        let (item_id, label_element, icon) = match rendered_entry {
1516            FsEntry::File(worktree_id, entry, ..) => {
1517                let name = self.entry_name(worktree_id, entry, cx);
1518                let color =
1519                    entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
1520                let icon = if settings.file_icons {
1521                    FileIcons::get_icon(&entry.path, cx)
1522                        .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
1523                } else {
1524                    None
1525                };
1526                (
1527                    ElementId::from(entry.id.to_proto() as usize),
1528                    HighlightedLabel::new(
1529                        name,
1530                        string_match
1531                            .map(|string_match| string_match.positions.clone())
1532                            .unwrap_or_default(),
1533                    )
1534                    .color(color)
1535                    .into_any_element(),
1536                    icon.unwrap_or_else(empty_icon),
1537                )
1538            }
1539            FsEntry::Directory(worktree_id, entry) => {
1540                let name = self.entry_name(worktree_id, entry, cx);
1541
1542                let is_expanded = !self
1543                    .collapsed_entries
1544                    .contains(&CollapsedEntry::Dir(*worktree_id, entry.id));
1545                let color =
1546                    entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
1547                let icon = if settings.folder_icons {
1548                    FileIcons::get_folder_icon(is_expanded, cx)
1549                } else {
1550                    FileIcons::get_chevron_icon(is_expanded, cx)
1551                }
1552                .map(Icon::from_path)
1553                .map(|icon| icon.color(color).into_any_element());
1554                (
1555                    ElementId::from(entry.id.to_proto() as usize),
1556                    HighlightedLabel::new(
1557                        name,
1558                        string_match
1559                            .map(|string_match| string_match.positions.clone())
1560                            .unwrap_or_default(),
1561                    )
1562                    .color(color)
1563                    .into_any_element(),
1564                    icon.unwrap_or_else(empty_icon),
1565                )
1566            }
1567            FsEntry::ExternalFile(buffer_id, ..) => {
1568                let color = entry_label_color(is_active);
1569                let (icon, name) = match self.buffer_snapshot_for_id(*buffer_id, cx) {
1570                    Some(buffer_snapshot) => match buffer_snapshot.file() {
1571                        Some(file) => {
1572                            let path = file.path();
1573                            let icon = if settings.file_icons {
1574                                FileIcons::get_icon(path.as_ref(), cx)
1575                            } else {
1576                                None
1577                            }
1578                            .map(Icon::from_path)
1579                            .map(|icon| icon.color(color).into_any_element());
1580                            (icon, file_name(path.as_ref()))
1581                        }
1582                        None => (None, "Untitled".to_string()),
1583                    },
1584                    None => (None, "Unknown buffer".to_string()),
1585                };
1586                (
1587                    ElementId::from(buffer_id.to_proto() as usize),
1588                    HighlightedLabel::new(
1589                        name,
1590                        string_match
1591                            .map(|string_match| string_match.positions.clone())
1592                            .unwrap_or_default(),
1593                    )
1594                    .color(color)
1595                    .into_any_element(),
1596                    icon.unwrap_or_else(empty_icon),
1597                )
1598            }
1599        };
1600
1601        self.entry_element(
1602            PanelEntry::Fs(rendered_entry.clone()),
1603            item_id,
1604            depth,
1605            Some(icon),
1606            is_active,
1607            label_element,
1608            cx,
1609        )
1610    }
1611
1612    fn render_folded_dirs(
1613        &self,
1614        worktree_id: WorktreeId,
1615        dir_entries: &[Entry],
1616        depth: usize,
1617        string_match: Option<&StringMatch>,
1618        cx: &mut ViewContext<OutlinePanel>,
1619    ) -> Stateful<Div> {
1620        let settings = OutlinePanelSettings::get_global(cx);
1621        let is_active = match self.selected_entry() {
1622            Some(PanelEntry::FoldedDirs(selected_worktree_id, selected_entries)) => {
1623                selected_worktree_id == &worktree_id && selected_entries == dir_entries
1624            }
1625            _ => false,
1626        };
1627        let (item_id, label_element, icon) = {
1628            let name = self.dir_names_string(dir_entries, worktree_id, cx);
1629
1630            let is_expanded = dir_entries.iter().all(|dir| {
1631                !self
1632                    .collapsed_entries
1633                    .contains(&CollapsedEntry::Dir(worktree_id, dir.id))
1634            });
1635            let is_ignored = dir_entries.iter().any(|entry| entry.is_ignored);
1636            let git_status = dir_entries.first().and_then(|entry| entry.git_status);
1637            let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
1638            let icon = if settings.folder_icons {
1639                FileIcons::get_folder_icon(is_expanded, cx)
1640            } else {
1641                FileIcons::get_chevron_icon(is_expanded, cx)
1642            }
1643            .map(Icon::from_path)
1644            .map(|icon| icon.color(color).into_any_element());
1645            (
1646                ElementId::from(
1647                    dir_entries
1648                        .last()
1649                        .map(|entry| entry.id.to_proto())
1650                        .unwrap_or_else(|| worktree_id.to_proto()) as usize,
1651                ),
1652                HighlightedLabel::new(
1653                    name,
1654                    string_match
1655                        .map(|string_match| string_match.positions.clone())
1656                        .unwrap_or_default(),
1657                )
1658                .color(color)
1659                .into_any_element(),
1660                icon.unwrap_or_else(empty_icon),
1661            )
1662        };
1663
1664        self.entry_element(
1665            PanelEntry::FoldedDirs(worktree_id, dir_entries.to_vec()),
1666            item_id,
1667            depth,
1668            Some(icon),
1669            is_active,
1670            label_element,
1671            cx,
1672        )
1673    }
1674
1675    fn render_search_match(
1676        &self,
1677        match_range: &Range<editor::Anchor>,
1678        search_data: &SearchData,
1679        kind: SearchKind,
1680        depth: usize,
1681        string_match: Option<&StringMatch>,
1682        cx: &mut ViewContext<Self>,
1683    ) -> Stateful<Div> {
1684        let search_matches = string_match
1685            .iter()
1686            .flat_map(|string_match| string_match.ranges())
1687            .collect::<Vec<_>>();
1688        let match_ranges = if search_matches.is_empty() {
1689            &search_data.search_match_indices
1690        } else {
1691            &search_matches
1692        };
1693        let label_element = language::render_item(
1694            &OutlineItem {
1695                depth,
1696                annotation_range: None,
1697                range: search_data.context_range.clone(),
1698                text: search_data.context_text.clone(),
1699                highlight_ranges: search_data.highlight_ranges.clone(),
1700                name_ranges: search_data.search_match_indices.clone(),
1701                body_range: Some(search_data.context_range.clone()),
1702            },
1703            match_ranges.into_iter().cloned(),
1704            cx,
1705        )
1706        .into_any_element();
1707
1708        let is_active = match self.selected_entry() {
1709            Some(PanelEntry::Search(SearchEntry {
1710                match_range: selected_match_range,
1711                ..
1712            })) => match_range == selected_match_range,
1713            _ => false,
1714        };
1715        self.entry_element(
1716            PanelEntry::Search(SearchEntry {
1717                kind,
1718                match_range: match_range.clone(),
1719                same_line_matches: Vec::new(),
1720                render_data: Some(OnceCell::new()),
1721            }),
1722            ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
1723            depth,
1724            None,
1725            is_active,
1726            label_element,
1727            cx,
1728        )
1729    }
1730
1731    #[allow(clippy::too_many_arguments)]
1732    fn entry_element(
1733        &self,
1734        rendered_entry: PanelEntry,
1735        item_id: ElementId,
1736        depth: usize,
1737        icon_element: Option<AnyElement>,
1738        is_active: bool,
1739        label_element: gpui::AnyElement,
1740        cx: &mut ViewContext<OutlinePanel>,
1741    ) -> Stateful<Div> {
1742        let settings = OutlinePanelSettings::get_global(cx);
1743        div()
1744            .text_ui(cx)
1745            .id(item_id.clone())
1746            .child(
1747                ListItem::new(item_id)
1748                    .indent_level(depth)
1749                    .indent_step_size(px(settings.indent_size))
1750                    .selected(is_active)
1751                    .when_some(icon_element, |list_item, icon_element| {
1752                        list_item.child(h_flex().child(icon_element))
1753                    })
1754                    .child(h_flex().h_6().child(label_element).ml_1())
1755                    .on_click({
1756                        let clicked_entry = rendered_entry.clone();
1757                        cx.listener(move |outline_panel, event: &gpui::ClickEvent, cx| {
1758                            if event.down.button == MouseButton::Right || event.down.first_mouse {
1759                                return;
1760                            }
1761                            let change_selection = event.down.click_count > 1;
1762                            outline_panel.open_entry(&clicked_entry, change_selection, cx);
1763                        })
1764                    })
1765                    .on_secondary_mouse_down(cx.listener(
1766                        move |outline_panel, event: &MouseDownEvent, cx| {
1767                            // Stop propagation to prevent the catch-all context menu for the project
1768                            // panel from being deployed.
1769                            cx.stop_propagation();
1770                            outline_panel.deploy_context_menu(
1771                                event.position,
1772                                rendered_entry.clone(),
1773                                cx,
1774                            )
1775                        },
1776                    )),
1777            )
1778            .border_1()
1779            .border_r_2()
1780            .rounded_none()
1781            .hover(|style| {
1782                if is_active {
1783                    style
1784                } else {
1785                    let hover_color = cx.theme().colors().ghost_element_hover;
1786                    style.bg(hover_color).border_color(hover_color)
1787                }
1788            })
1789            .when(is_active && self.focus_handle.contains_focused(cx), |div| {
1790                div.border_color(Color::Selected.color(cx))
1791            })
1792    }
1793
1794    fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &AppContext) -> String {
1795        let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
1796            Some(worktree) => {
1797                let worktree = worktree.read(cx);
1798                match worktree.snapshot().root_entry() {
1799                    Some(root_entry) => {
1800                        if root_entry.id == entry.id {
1801                            file_name(worktree.abs_path().as_ref())
1802                        } else {
1803                            let path = worktree.absolutize(entry.path.as_ref()).ok();
1804                            let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
1805                            file_name(path)
1806                        }
1807                    }
1808                    None => {
1809                        let path = worktree.absolutize(entry.path.as_ref()).ok();
1810                        let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
1811                        file_name(path)
1812                    }
1813                }
1814            }
1815            None => file_name(entry.path.as_ref()),
1816        };
1817        name
1818    }
1819
1820    fn update_fs_entries(
1821        &mut self,
1822        active_editor: &View<Editor>,
1823        new_entries: HashSet<ExcerptId>,
1824        debounce: Option<Duration>,
1825        cx: &mut ViewContext<Self>,
1826    ) {
1827        if !self.active {
1828            return;
1829        }
1830
1831        let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
1832        let active_multi_buffer = active_editor.read(cx).buffer().clone();
1833        let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
1834        let mut new_collapsed_entries = self.collapsed_entries.clone();
1835        let mut new_unfolded_dirs = self.unfolded_dirs.clone();
1836        let mut root_entries = HashSet::default();
1837        let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
1838        let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
1839            HashMap::default(),
1840            |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
1841                let buffer_id = buffer_snapshot.remote_id();
1842                let file = File::from_dyn(buffer_snapshot.file());
1843                let entry_id = file.and_then(|file| file.project_entry_id(cx));
1844                let worktree = file.map(|file| file.worktree.read(cx).snapshot());
1845                let is_new =
1846                    new_entries.contains(&excerpt_id) || !self.excerpts.contains_key(&buffer_id);
1847                buffer_excerpts
1848                    .entry(buffer_id)
1849                    .or_insert_with(|| (is_new, Vec::new(), entry_id, worktree))
1850                    .1
1851                    .push(excerpt_id);
1852
1853                let outlines = match self
1854                    .excerpts
1855                    .get(&buffer_id)
1856                    .and_then(|excerpts| excerpts.get(&excerpt_id))
1857                {
1858                    Some(old_excerpt) => match &old_excerpt.outlines {
1859                        ExcerptOutlines::Outlines(outlines) => {
1860                            ExcerptOutlines::Outlines(outlines.clone())
1861                        }
1862                        ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
1863                        ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
1864                    },
1865                    None => ExcerptOutlines::NotFetched,
1866                };
1867                new_excerpts.entry(buffer_id).or_default().insert(
1868                    excerpt_id,
1869                    Excerpt {
1870                        range: excerpt_range,
1871                        outlines,
1872                    },
1873                );
1874                buffer_excerpts
1875            },
1876        );
1877
1878        self.updating_fs_entries = true;
1879        self.fs_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
1880            if let Some(debounce) = debounce {
1881                cx.background_executor().timer(debounce).await;
1882            }
1883            let Some((
1884                new_collapsed_entries,
1885                new_unfolded_dirs,
1886                new_fs_entries,
1887                new_depth_map,
1888                new_children_count,
1889            )) = cx
1890                .background_executor()
1891                .spawn(async move {
1892                    let mut processed_external_buffers = HashSet::default();
1893                    let mut new_worktree_entries =
1894                        HashMap::<WorktreeId, (worktree::Snapshot, HashSet<Entry>)>::default();
1895                    let mut worktree_excerpts = HashMap::<
1896                        WorktreeId,
1897                        HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
1898                    >::default();
1899                    let mut external_excerpts = HashMap::default();
1900
1901                    for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
1902                        if is_new {
1903                            match &worktree {
1904                                Some(worktree) => {
1905                                    new_collapsed_entries
1906                                        .remove(&CollapsedEntry::File(worktree.id(), buffer_id));
1907                                }
1908                                None => {
1909                                    new_collapsed_entries
1910                                        .remove(&CollapsedEntry::ExternalFile(buffer_id));
1911                                }
1912                            }
1913                        }
1914
1915                        if let Some(worktree) = worktree {
1916                            let worktree_id = worktree.id();
1917                            let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
1918
1919                            match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
1920                                Some(entry) => {
1921                                    let mut traversal = worktree.traverse_from_path(
1922                                        true,
1923                                        true,
1924                                        true,
1925                                        entry.path.as_ref(),
1926                                    );
1927
1928                                    let mut entries_to_add = HashSet::default();
1929                                    worktree_excerpts
1930                                        .entry(worktree_id)
1931                                        .or_default()
1932                                        .insert(entry.id, (buffer_id, excerpts));
1933                                    let mut current_entry = entry;
1934                                    loop {
1935                                        if current_entry.is_dir() {
1936                                            let is_root =
1937                                                worktree.root_entry().map(|entry| entry.id)
1938                                                    == Some(current_entry.id);
1939                                            if is_root {
1940                                                root_entries.insert(current_entry.id);
1941                                                if auto_fold_dirs {
1942                                                    unfolded_dirs.insert(current_entry.id);
1943                                                }
1944                                            }
1945                                            if is_new {
1946                                                new_collapsed_entries.remove(&CollapsedEntry::Dir(
1947                                                    worktree_id,
1948                                                    current_entry.id,
1949                                                ));
1950                                            }
1951                                        }
1952
1953                                        let new_entry_added = entries_to_add.insert(current_entry);
1954                                        if new_entry_added && traversal.back_to_parent() {
1955                                            if let Some(parent_entry) = traversal.entry() {
1956                                                current_entry = parent_entry.clone();
1957                                                continue;
1958                                            }
1959                                        }
1960                                        break;
1961                                    }
1962                                    new_worktree_entries
1963                                        .entry(worktree_id)
1964                                        .or_insert_with(|| (worktree.clone(), HashSet::default()))
1965                                        .1
1966                                        .extend(entries_to_add);
1967                                }
1968                                None => {
1969                                    if processed_external_buffers.insert(buffer_id) {
1970                                        external_excerpts
1971                                            .entry(buffer_id)
1972                                            .or_insert_with(|| Vec::new())
1973                                            .extend(excerpts);
1974                                    }
1975                                }
1976                            }
1977                        } else if processed_external_buffers.insert(buffer_id) {
1978                            external_excerpts
1979                                .entry(buffer_id)
1980                                .or_insert_with(|| Vec::new())
1981                                .extend(excerpts);
1982                        }
1983                    }
1984
1985                    let mut new_children_count =
1986                        HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
1987
1988                    let worktree_entries = new_worktree_entries
1989                        .into_iter()
1990                        .map(|(worktree_id, (worktree_snapshot, entries))| {
1991                            let mut entries = entries.into_iter().collect::<Vec<_>>();
1992                            // For a proper git status propagation, we have to keep the entries sorted lexicographically.
1993                            entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
1994                            worktree_snapshot.propagate_git_statuses(&mut entries);
1995                            project::sort_worktree_entries(&mut entries);
1996                            (worktree_id, entries)
1997                        })
1998                        .flat_map(|(worktree_id, entries)| {
1999                            {
2000                                entries
2001                                    .into_iter()
2002                                    .filter_map(|entry| {
2003                                        if auto_fold_dirs {
2004                                            if let Some(parent) = entry.path.parent() {
2005                                                let children = new_children_count
2006                                                    .entry(worktree_id)
2007                                                    .or_default()
2008                                                    .entry(Arc::from(parent))
2009                                                    .or_default();
2010                                                if entry.is_dir() {
2011                                                    children.dirs += 1;
2012                                                } else {
2013                                                    children.files += 1;
2014                                                }
2015                                            }
2016                                        }
2017
2018                                        if entry.is_dir() {
2019                                            Some(FsEntry::Directory(worktree_id, entry))
2020                                        } else {
2021                                            let (buffer_id, excerpts) = worktree_excerpts
2022                                                .get_mut(&worktree_id)
2023                                                .and_then(|worktree_excerpts| {
2024                                                    worktree_excerpts.remove(&entry.id)
2025                                                })?;
2026                                            Some(FsEntry::File(
2027                                                worktree_id,
2028                                                entry,
2029                                                buffer_id,
2030                                                excerpts,
2031                                            ))
2032                                        }
2033                                    })
2034                                    .collect::<Vec<_>>()
2035                            }
2036                        })
2037                        .collect::<Vec<_>>();
2038
2039                    let mut visited_dirs = Vec::new();
2040                    let mut new_depth_map = HashMap::default();
2041                    let new_visible_entries = external_excerpts
2042                        .into_iter()
2043                        .sorted_by_key(|(id, _)| *id)
2044                        .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts))
2045                        .chain(worktree_entries)
2046                        .filter(|visible_item| {
2047                            match visible_item {
2048                                FsEntry::Directory(worktree_id, dir_entry) => {
2049                                    let parent_id = back_to_common_visited_parent(
2050                                        &mut visited_dirs,
2051                                        worktree_id,
2052                                        dir_entry,
2053                                    );
2054
2055                                    let depth = if root_entries.contains(&dir_entry.id) {
2056                                        0
2057                                    } else {
2058                                        if auto_fold_dirs {
2059                                            let children = new_children_count
2060                                                .get(&worktree_id)
2061                                                .and_then(|children_count| {
2062                                                    children_count.get(&dir_entry.path)
2063                                                })
2064                                                .copied()
2065                                                .unwrap_or_default();
2066
2067                                            if !children.may_be_fold_part()
2068                                                || (children.dirs == 0
2069                                                    && visited_dirs
2070                                                        .last()
2071                                                        .map(|(parent_dir_id, _)| {
2072                                                            new_unfolded_dirs
2073                                                                .get(&worktree_id)
2074                                                                .map_or(true, |unfolded_dirs| {
2075                                                                    unfolded_dirs
2076                                                                        .contains(&parent_dir_id)
2077                                                                })
2078                                                        })
2079                                                        .unwrap_or(true))
2080                                            {
2081                                                new_unfolded_dirs
2082                                                    .entry(*worktree_id)
2083                                                    .or_default()
2084                                                    .insert(dir_entry.id);
2085                                            }
2086                                        }
2087
2088                                        parent_id
2089                                            .and_then(|(worktree_id, id)| {
2090                                                new_depth_map.get(&(worktree_id, id)).copied()
2091                                            })
2092                                            .unwrap_or(0)
2093                                            + 1
2094                                    };
2095                                    visited_dirs.push((dir_entry.id, dir_entry.path.clone()));
2096                                    new_depth_map.insert((*worktree_id, dir_entry.id), depth);
2097                                }
2098                                FsEntry::File(worktree_id, file_entry, ..) => {
2099                                    let parent_id = back_to_common_visited_parent(
2100                                        &mut visited_dirs,
2101                                        worktree_id,
2102                                        file_entry,
2103                                    );
2104                                    let depth = if root_entries.contains(&file_entry.id) {
2105                                        0
2106                                    } else {
2107                                        parent_id
2108                                            .and_then(|(worktree_id, id)| {
2109                                                new_depth_map.get(&(worktree_id, id)).copied()
2110                                            })
2111                                            .unwrap_or(0)
2112                                            + 1
2113                                    };
2114                                    new_depth_map.insert((*worktree_id, file_entry.id), depth);
2115                                }
2116                                FsEntry::ExternalFile(..) => {
2117                                    visited_dirs.clear();
2118                                }
2119                            }
2120
2121                            true
2122                        })
2123                        .collect::<Vec<_>>();
2124
2125                    anyhow::Ok((
2126                        new_collapsed_entries,
2127                        new_unfolded_dirs,
2128                        new_visible_entries,
2129                        new_depth_map,
2130                        new_children_count,
2131                    ))
2132                })
2133                .await
2134                .log_err()
2135            else {
2136                return;
2137            };
2138
2139            outline_panel
2140                .update(&mut cx, |outline_panel, cx| {
2141                    outline_panel.updating_fs_entries = false;
2142                    outline_panel.excerpts = new_excerpts;
2143                    outline_panel.collapsed_entries = new_collapsed_entries;
2144                    outline_panel.unfolded_dirs = new_unfolded_dirs;
2145                    outline_panel.fs_entries = new_fs_entries;
2146                    outline_panel.fs_entries_depth = new_depth_map;
2147                    outline_panel.fs_children_count = new_children_count;
2148                    outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
2149                    outline_panel.update_non_fs_items(cx);
2150
2151                    cx.notify();
2152                })
2153                .ok();
2154        });
2155    }
2156
2157    fn replace_active_editor(
2158        &mut self,
2159        new_active_editor: View<Editor>,
2160        cx: &mut ViewContext<Self>,
2161    ) {
2162        self.clear_previous(cx);
2163        let buffer_search_subscription = cx.subscribe(
2164            &new_active_editor,
2165            |outline_panel: &mut Self, _, _: &SearchEvent, cx: &mut ViewContext<'_, Self>| {
2166                outline_panel.update_search_matches(cx);
2167                outline_panel.autoscroll(cx);
2168            },
2169        );
2170        self.active_item = Some(ActiveItem {
2171            _buffer_search_subscription: buffer_search_subscription,
2172            _editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, cx),
2173            active_editor: new_active_editor.downgrade(),
2174        });
2175        let new_entries =
2176            HashSet::from_iter(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
2177        self.selected_entry.invalidate();
2178        self.update_fs_entries(&new_active_editor, new_entries, None, cx);
2179    }
2180
2181    fn clear_previous(&mut self, cx: &mut WindowContext<'_>) {
2182        self.filter_editor.update(cx, |editor, cx| editor.clear(cx));
2183        self.collapsed_entries.clear();
2184        self.unfolded_dirs.clear();
2185        self.selected_entry = SelectedEntry::None;
2186        self.fs_entries_update_task = Task::ready(());
2187        self.cached_entries_update_task = Task::ready(());
2188        self.active_item = None;
2189        self.fs_entries.clear();
2190        self.fs_entries_depth.clear();
2191        self.fs_children_count.clear();
2192        self.outline_fetch_tasks.clear();
2193        self.excerpts.clear();
2194        self.cached_entries = Vec::new();
2195        self.search_matches.clear();
2196        self.search = None;
2197        self.pinned = false;
2198    }
2199
2200    fn location_for_editor_selection(
2201        &mut self,
2202        editor: &View<Editor>,
2203        cx: &mut ViewContext<Self>,
2204    ) -> Option<PanelEntry> {
2205        let selection = editor
2206            .read(cx)
2207            .selections
2208            .newest::<language::Point>(cx)
2209            .head();
2210        let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx));
2211        let multi_buffer = editor.read(cx).buffer();
2212        let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
2213        let (excerpt_id, buffer, _) = editor
2214            .read(cx)
2215            .buffer()
2216            .read(cx)
2217            .excerpt_containing(selection, cx)?;
2218        let buffer_id = buffer.read(cx).remote_id();
2219        let selection_display_point = selection.to_display_point(&editor_snapshot);
2220
2221        match self.mode {
2222            ItemsDisplayMode::Search => self
2223                .search_matches
2224                .iter()
2225                .rev()
2226                .min_by_key(|&match_range| {
2227                    let match_display_range =
2228                        match_range.clone().to_display_points(&editor_snapshot);
2229                    let start_distance = if selection_display_point < match_display_range.start {
2230                        match_display_range.start - selection_display_point
2231                    } else {
2232                        selection_display_point - match_display_range.start
2233                    };
2234                    let end_distance = if selection_display_point < match_display_range.end {
2235                        match_display_range.end - selection_display_point
2236                    } else {
2237                        selection_display_point - match_display_range.end
2238                    };
2239                    start_distance + end_distance
2240                })
2241                .and_then(|closest_range| {
2242                    self.cached_entries.iter().find_map(|cached_entry| {
2243                        if let PanelEntry::Search(SearchEntry {
2244                            match_range,
2245                            same_line_matches,
2246                            ..
2247                        }) = &cached_entry.entry
2248                        {
2249                            if match_range == closest_range
2250                                || same_line_matches.contains(&closest_range)
2251                            {
2252                                Some(cached_entry.entry.clone())
2253                            } else {
2254                                None
2255                            }
2256                        } else {
2257                            None
2258                        }
2259                    })
2260                }),
2261            ItemsDisplayMode::Outline => self.outline_location(
2262                buffer_id,
2263                excerpt_id,
2264                multi_buffer_snapshot,
2265                editor_snapshot,
2266                selection_display_point,
2267            ),
2268        }
2269    }
2270
2271    fn outline_location(
2272        &mut self,
2273        buffer_id: BufferId,
2274        excerpt_id: ExcerptId,
2275        multi_buffer_snapshot: editor::MultiBufferSnapshot,
2276        editor_snapshot: editor::EditorSnapshot,
2277        selection_display_point: DisplayPoint,
2278    ) -> Option<PanelEntry> {
2279        let excerpt_outlines = self
2280            .excerpts
2281            .get(&buffer_id)
2282            .and_then(|excerpts| excerpts.get(&excerpt_id))
2283            .into_iter()
2284            .flat_map(|excerpt| excerpt.iter_outlines())
2285            .flat_map(|outline| {
2286                let start = multi_buffer_snapshot
2287                    .anchor_in_excerpt(excerpt_id, outline.range.start)?
2288                    .to_display_point(&editor_snapshot);
2289                let end = multi_buffer_snapshot
2290                    .anchor_in_excerpt(excerpt_id, outline.range.end)?
2291                    .to_display_point(&editor_snapshot);
2292                Some((start..end, outline))
2293            })
2294            .collect::<Vec<_>>();
2295
2296        let mut matching_outline_indices = Vec::new();
2297        let mut children = HashMap::default();
2298        let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
2299
2300        for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
2301            if outline_range
2302                .to_inclusive()
2303                .contains(&selection_display_point)
2304            {
2305                matching_outline_indices.push(i);
2306            } else if (outline_range.start.row()..outline_range.end.row())
2307                .to_inclusive()
2308                .contains(&selection_display_point.row())
2309            {
2310                matching_outline_indices.push(i);
2311            }
2312
2313            while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
2314                if parent_outline.depth >= outline.depth
2315                    || !parent_range.contains(&outline_range.start)
2316                {
2317                    parents_stack.pop();
2318                } else {
2319                    break;
2320                }
2321            }
2322            if let Some((_, _, parent_index)) = parents_stack.last_mut() {
2323                children
2324                    .entry(*parent_index)
2325                    .or_insert_with(Vec::new)
2326                    .push(i);
2327            }
2328            parents_stack.push((outline_range, outline, i));
2329        }
2330
2331        let outline_item = matching_outline_indices
2332            .into_iter()
2333            .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
2334            .filter(|(i, _)| {
2335                children
2336                    .get(i)
2337                    .map(|children| {
2338                        children.iter().all(|child_index| {
2339                            excerpt_outlines
2340                                .get(*child_index)
2341                                .map(|(child_range, _)| child_range.start > selection_display_point)
2342                                .unwrap_or(false)
2343                        })
2344                    })
2345                    .unwrap_or(true)
2346            })
2347            .min_by_key(|(_, (outline_range, outline))| {
2348                let distance_from_start = if outline_range.start > selection_display_point {
2349                    outline_range.start - selection_display_point
2350                } else {
2351                    selection_display_point - outline_range.start
2352                };
2353                let distance_from_end = if outline_range.end > selection_display_point {
2354                    outline_range.end - selection_display_point
2355                } else {
2356                    selection_display_point - outline_range.end
2357                };
2358
2359                (
2360                    cmp::Reverse(outline.depth),
2361                    distance_from_start + distance_from_end,
2362                )
2363            })
2364            .map(|(_, (_, outline))| *outline)
2365            .cloned();
2366
2367        let closest_container = match outline_item {
2368            Some(outline) => {
2369                PanelEntry::Outline(OutlineEntry::Outline(buffer_id, excerpt_id, outline))
2370            }
2371            None => {
2372                self.cached_entries.iter().rev().find_map(|cached_entry| {
2373                    match &cached_entry.entry {
2374                        PanelEntry::Outline(OutlineEntry::Excerpt(
2375                            entry_buffer_id,
2376                            entry_excerpt_id,
2377                            _,
2378                        )) => {
2379                            if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id {
2380                                Some(cached_entry.entry.clone())
2381                            } else {
2382                                None
2383                            }
2384                        }
2385                        PanelEntry::Fs(
2386                            FsEntry::ExternalFile(file_buffer_id, file_excerpts)
2387                            | FsEntry::File(_, _, file_buffer_id, file_excerpts),
2388                        ) => {
2389                            if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
2390                                Some(cached_entry.entry.clone())
2391                            } else {
2392                                None
2393                            }
2394                        }
2395                        _ => None,
2396                    }
2397                })?
2398            }
2399        };
2400        Some(closest_container)
2401    }
2402
2403    fn fetch_outdated_outlines(&mut self, cx: &mut ViewContext<Self>) {
2404        let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
2405        if excerpt_fetch_ranges.is_empty() {
2406            return;
2407        }
2408
2409        let syntax_theme = cx.theme().syntax().clone();
2410        for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
2411            for (excerpt_id, excerpt_range) in excerpt_ranges {
2412                let syntax_theme = syntax_theme.clone();
2413                let buffer_snapshot = buffer_snapshot.clone();
2414                self.outline_fetch_tasks.insert(
2415                    (buffer_id, excerpt_id),
2416                    cx.spawn(|outline_panel, mut cx| async move {
2417                        let fetched_outlines = cx
2418                            .background_executor()
2419                            .spawn(async move {
2420                                buffer_snapshot
2421                                    .outline_items_containing(
2422                                        excerpt_range.context,
2423                                        false,
2424                                        Some(&syntax_theme),
2425                                    )
2426                                    .unwrap_or_default()
2427                            })
2428                            .await;
2429                        outline_panel
2430                            .update(&mut cx, |outline_panel, cx| {
2431                                if let Some(excerpt) = outline_panel
2432                                    .excerpts
2433                                    .entry(buffer_id)
2434                                    .or_default()
2435                                    .get_mut(&excerpt_id)
2436                                {
2437                                    excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
2438                                }
2439                                outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
2440                            })
2441                            .ok();
2442                    }),
2443                );
2444            }
2445        }
2446    }
2447
2448    fn is_singleton_active(&self, cx: &AppContext) -> bool {
2449        self.active_editor().map_or(false, |active_editor| {
2450            active_editor.read(cx).buffer().read(cx).is_singleton()
2451        })
2452    }
2453
2454    fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
2455        self.outline_fetch_tasks.clear();
2456        let mut ids = ids.into_iter().collect::<HashSet<_>>();
2457        for excerpts in self.excerpts.values_mut() {
2458            ids.retain(|id| {
2459                if let Some(excerpt) = excerpts.get_mut(id) {
2460                    excerpt.invalidate_outlines();
2461                    false
2462                } else {
2463                    true
2464                }
2465            });
2466            if ids.is_empty() {
2467                break;
2468            }
2469        }
2470    }
2471
2472    fn excerpt_fetch_ranges(
2473        &self,
2474        cx: &AppContext,
2475    ) -> HashMap<
2476        BufferId,
2477        (
2478            BufferSnapshot,
2479            HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
2480        ),
2481    > {
2482        self.fs_entries
2483            .iter()
2484            .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
2485                match fs_entry {
2486                    FsEntry::File(_, _, buffer_id, file_excerpts)
2487                    | FsEntry::ExternalFile(buffer_id, file_excerpts) => {
2488                        let excerpts = self.excerpts.get(&buffer_id);
2489                        for &file_excerpt in file_excerpts {
2490                            if let Some(excerpt) = excerpts
2491                                .and_then(|excerpts| excerpts.get(&file_excerpt))
2492                                .filter(|excerpt| excerpt.should_fetch_outlines())
2493                            {
2494                                match excerpts_to_fetch.entry(*buffer_id) {
2495                                    hash_map::Entry::Occupied(mut o) => {
2496                                        o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
2497                                    }
2498                                    hash_map::Entry::Vacant(v) => {
2499                                        if let Some(buffer_snapshot) =
2500                                            self.buffer_snapshot_for_id(*buffer_id, cx)
2501                                        {
2502                                            v.insert((buffer_snapshot, HashMap::default()))
2503                                                .1
2504                                                .insert(file_excerpt, excerpt.range.clone());
2505                                        }
2506                                    }
2507                                }
2508                            }
2509                        }
2510                    }
2511                    FsEntry::Directory(..) => {}
2512                }
2513                excerpts_to_fetch
2514            })
2515    }
2516
2517    fn buffer_snapshot_for_id(
2518        &self,
2519        buffer_id: BufferId,
2520        cx: &AppContext,
2521    ) -> Option<BufferSnapshot> {
2522        let editor = self.active_editor()?;
2523        Some(
2524            editor
2525                .read(cx)
2526                .buffer()
2527                .read(cx)
2528                .buffer(buffer_id)?
2529                .read(cx)
2530                .snapshot(),
2531        )
2532    }
2533
2534    fn abs_path(&self, entry: &PanelEntry, cx: &AppContext) -> Option<PathBuf> {
2535        match entry {
2536            PanelEntry::Fs(
2537                FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _),
2538            ) => self
2539                .buffer_snapshot_for_id(*buffer_id, cx)
2540                .and_then(|buffer_snapshot| {
2541                    let file = File::from_dyn(buffer_snapshot.file())?;
2542                    file.worktree.read(cx).absolutize(&file.path).ok()
2543                }),
2544            PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => self
2545                .project
2546                .read(cx)
2547                .worktree_for_id(*worktree_id, cx)?
2548                .read(cx)
2549                .absolutize(&entry.path)
2550                .ok(),
2551            PanelEntry::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
2552                self.project
2553                    .read(cx)
2554                    .worktree_for_id(*worktree_id, cx)
2555                    .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
2556            }),
2557            PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
2558        }
2559    }
2560
2561    fn relative_path(&self, entry: &FsEntry, cx: &AppContext) -> Option<Arc<Path>> {
2562        match entry {
2563            FsEntry::ExternalFile(buffer_id, _) => {
2564                let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
2565                Some(buffer_snapshot.file()?.path().clone())
2566            }
2567            FsEntry::Directory(_, entry) => Some(entry.path.clone()),
2568            FsEntry::File(_, entry, ..) => Some(entry.path.clone()),
2569        }
2570    }
2571
2572    fn update_cached_entries(
2573        &mut self,
2574        debounce: Option<Duration>,
2575        cx: &mut ViewContext<OutlinePanel>,
2576    ) {
2577        if !self.active {
2578            return;
2579        }
2580
2581        let is_singleton = self.is_singleton_active(cx);
2582        let query = self.query(cx);
2583        self.cached_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
2584            if let Some(debounce) = debounce {
2585                cx.background_executor().timer(debounce).await;
2586            }
2587            let Some(new_cached_entries) = outline_panel
2588                .update(&mut cx, |outline_panel, cx| {
2589                    outline_panel.generate_cached_entries(is_singleton, query, cx)
2590                })
2591                .ok()
2592            else {
2593                return;
2594            };
2595            let new_cached_entries = new_cached_entries.await;
2596            outline_panel
2597                .update(&mut cx, |outline_panel, cx| {
2598                    outline_panel.cached_entries = new_cached_entries;
2599                    if outline_panel.selected_entry.is_invalidated() {
2600                        if let Some(new_selected_entry) =
2601                            outline_panel.active_editor().and_then(|active_editor| {
2602                                outline_panel.location_for_editor_selection(&active_editor, cx)
2603                            })
2604                        {
2605                            outline_panel.select_entry(new_selected_entry, false, cx);
2606                        }
2607                    }
2608
2609                    outline_panel.autoscroll(cx);
2610                    cx.notify();
2611                })
2612                .ok();
2613        });
2614    }
2615
2616    fn generate_cached_entries(
2617        &self,
2618        is_singleton: bool,
2619        query: Option<String>,
2620        cx: &mut ViewContext<'_, Self>,
2621    ) -> Task<Vec<CachedEntry>> {
2622        let project = self.project.clone();
2623        cx.spawn(|outline_panel, mut cx| async move {
2624            let mut entries = Vec::new();
2625            let mut match_candidates = Vec::new();
2626
2627            let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| {
2628                let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2629                let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec<Entry>)>;
2630                let track_matches = query.is_some();
2631                let mut parent_dirs = Vec::<(&Path, bool, bool, usize)>::new();
2632
2633                for entry in &outline_panel.fs_entries {
2634                    let is_expanded = outline_panel.is_expanded(entry);
2635                    let (depth, should_add) = match entry {
2636                        FsEntry::Directory(worktree_id, dir_entry) => {
2637                            let is_root = project
2638                                .read(cx)
2639                                .worktree_for_id(*worktree_id, cx)
2640                                .map_or(false, |worktree| {
2641                                    worktree.read(cx).root_entry() == Some(dir_entry)
2642                                });
2643                            let folded = auto_fold_dirs
2644                                && !is_root
2645                                && outline_panel
2646                                    .unfolded_dirs
2647                                    .get(worktree_id)
2648                                    .map_or(true, |unfolded_dirs| {
2649                                        !unfolded_dirs.contains(&dir_entry.id)
2650                                    });
2651                            let fs_depth = outline_panel
2652                                .fs_entries_depth
2653                                .get(&(*worktree_id, dir_entry.id))
2654                                .copied()
2655                                .unwrap_or(0);
2656                            while let Some(&(previous_path, ..)) = parent_dirs.last() {
2657                                if dir_entry.path.starts_with(previous_path) {
2658                                    break;
2659                                }
2660                                parent_dirs.pop();
2661                            }
2662                            let auto_fold = match parent_dirs.last() {
2663                                Some((parent_path, parent_folded, _, _)) => {
2664                                    *parent_folded
2665                                        && Some(*parent_path) == dir_entry.path.parent()
2666                                        && outline_panel
2667                                            .fs_children_count
2668                                            .get(worktree_id)
2669                                            .and_then(|entries| entries.get(&dir_entry.path))
2670                                            .copied()
2671                                            .unwrap_or_default()
2672                                            .may_be_fold_part()
2673                                }
2674                                None => false,
2675                            };
2676                            let folded = folded || auto_fold;
2677                            let (depth, parent_expanded) = match parent_dirs.last() {
2678                                Some(&(_, previous_folded, previous_expanded, previous_depth)) => {
2679                                    let new_depth = if folded && previous_folded {
2680                                        previous_depth
2681                                    } else {
2682                                        previous_depth + 1
2683                                    };
2684                                    parent_dirs.push((
2685                                        &dir_entry.path,
2686                                        folded,
2687                                        previous_expanded && is_expanded,
2688                                        new_depth,
2689                                    ));
2690                                    (new_depth, previous_expanded)
2691                                }
2692                                None => {
2693                                    parent_dirs.push((
2694                                        &dir_entry.path,
2695                                        folded,
2696                                        is_expanded,
2697                                        fs_depth,
2698                                    ));
2699                                    (fs_depth, true)
2700                                }
2701                            };
2702
2703                            if let Some((folded_depth, folded_worktree_id, mut folded_dirs)) =
2704                                folded_dirs_entry.take()
2705                            {
2706                                if folded
2707                                    && worktree_id == &folded_worktree_id
2708                                    && dir_entry.path.parent()
2709                                        == folded_dirs.last().map(|entry| entry.path.as_ref())
2710                                {
2711                                    folded_dirs.push(dir_entry.clone());
2712                                    folded_dirs_entry =
2713                                        Some((folded_depth, folded_worktree_id, folded_dirs))
2714                                } else {
2715                                    if !is_singleton && (parent_expanded || query.is_some()) {
2716                                        let new_folded_dirs =
2717                                            PanelEntry::FoldedDirs(folded_worktree_id, folded_dirs);
2718                                        outline_panel.push_entry(
2719                                            &mut entries,
2720                                            &mut match_candidates,
2721                                            track_matches,
2722                                            new_folded_dirs,
2723                                            folded_depth,
2724                                            cx,
2725                                        );
2726                                    }
2727                                    folded_dirs_entry =
2728                                        Some((depth, *worktree_id, vec![dir_entry.clone()]))
2729                                }
2730                            } else if folded {
2731                                folded_dirs_entry =
2732                                    Some((depth, *worktree_id, vec![dir_entry.clone()]));
2733                            }
2734
2735                            let should_add = parent_expanded && folded_dirs_entry.is_none();
2736                            (depth, should_add)
2737                        }
2738                        FsEntry::ExternalFile(..) => {
2739                            if let Some((folded_depth, worktree_id, folded_dirs)) =
2740                                folded_dirs_entry.take()
2741                            {
2742                                let parent_expanded = parent_dirs
2743                                    .iter()
2744                                    .rev()
2745                                    .find(|(parent_path, ..)| {
2746                                        folded_dirs
2747                                            .iter()
2748                                            .all(|entry| entry.path.as_ref() != *parent_path)
2749                                    })
2750                                    .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
2751                                if !is_singleton && (parent_expanded || query.is_some()) {
2752                                    outline_panel.push_entry(
2753                                        &mut entries,
2754                                        &mut match_candidates,
2755                                        track_matches,
2756                                        PanelEntry::FoldedDirs(worktree_id, folded_dirs),
2757                                        folded_depth,
2758                                        cx,
2759                                    );
2760                                }
2761                            }
2762                            parent_dirs.clear();
2763                            (0, true)
2764                        }
2765                        FsEntry::File(worktree_id, file_entry, ..) => {
2766                            if let Some((folded_depth, worktree_id, folded_dirs)) =
2767                                folded_dirs_entry.take()
2768                            {
2769                                let parent_expanded = parent_dirs
2770                                    .iter()
2771                                    .rev()
2772                                    .find(|(parent_path, ..)| {
2773                                        folded_dirs
2774                                            .iter()
2775                                            .all(|entry| entry.path.as_ref() != *parent_path)
2776                                    })
2777                                    .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
2778                                if !is_singleton && (parent_expanded || query.is_some()) {
2779                                    outline_panel.push_entry(
2780                                        &mut entries,
2781                                        &mut match_candidates,
2782                                        track_matches,
2783                                        PanelEntry::FoldedDirs(worktree_id, folded_dirs),
2784                                        folded_depth,
2785                                        cx,
2786                                    );
2787                                }
2788                            }
2789
2790                            let fs_depth = outline_panel
2791                                .fs_entries_depth
2792                                .get(&(*worktree_id, file_entry.id))
2793                                .copied()
2794                                .unwrap_or(0);
2795                            while let Some(&(previous_path, ..)) = parent_dirs.last() {
2796                                if file_entry.path.starts_with(previous_path) {
2797                                    break;
2798                                }
2799                                parent_dirs.pop();
2800                            }
2801                            let (depth, should_add) = match parent_dirs.last() {
2802                                Some(&(_, _, previous_expanded, previous_depth)) => {
2803                                    let new_depth = previous_depth + 1;
2804                                    (new_depth, previous_expanded)
2805                                }
2806                                None => (fs_depth, true),
2807                            };
2808                            (depth, should_add)
2809                        }
2810                    };
2811
2812                    if !is_singleton
2813                        && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
2814                    {
2815                        outline_panel.push_entry(
2816                            &mut entries,
2817                            &mut match_candidates,
2818                            track_matches,
2819                            PanelEntry::Fs(entry.clone()),
2820                            depth,
2821                            cx,
2822                        );
2823                    }
2824
2825                    match outline_panel.mode {
2826                        ItemsDisplayMode::Search => {
2827                            if is_singleton || query.is_some() || (should_add && is_expanded) {
2828                                outline_panel.add_search_entries(
2829                                    entry,
2830                                    depth,
2831                                    track_matches,
2832                                    is_singleton,
2833                                    &mut entries,
2834                                    &mut match_candidates,
2835                                    cx,
2836                                );
2837                            }
2838                        }
2839                        ItemsDisplayMode::Outline => {
2840                            let excerpts_to_consider =
2841                                if is_singleton || query.is_some() || (should_add && is_expanded) {
2842                                    match entry {
2843                                        FsEntry::File(_, _, buffer_id, entry_excerpts) => {
2844                                            Some((*buffer_id, entry_excerpts))
2845                                        }
2846                                        FsEntry::ExternalFile(buffer_id, entry_excerpts) => {
2847                                            Some((*buffer_id, entry_excerpts))
2848                                        }
2849                                        _ => None,
2850                                    }
2851                                } else {
2852                                    None
2853                                };
2854                            if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
2855                                outline_panel.add_excerpt_entries(
2856                                    buffer_id,
2857                                    entry_excerpts,
2858                                    depth,
2859                                    track_matches,
2860                                    is_singleton,
2861                                    query.as_deref(),
2862                                    &mut entries,
2863                                    &mut match_candidates,
2864                                    cx,
2865                                );
2866                            }
2867                        }
2868                    }
2869
2870                    if is_singleton
2871                        && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
2872                        && !entries.iter().any(|item| {
2873                            matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
2874                        })
2875                    {
2876                        outline_panel.push_entry(
2877                            &mut entries,
2878                            &mut match_candidates,
2879                            track_matches,
2880                            PanelEntry::Fs(entry.clone()),
2881                            0,
2882                            cx,
2883                        );
2884                    }
2885                }
2886
2887                if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
2888                    let parent_expanded = parent_dirs
2889                        .iter()
2890                        .rev()
2891                        .find(|(parent_path, ..)| {
2892                            folded_dirs
2893                                .iter()
2894                                .all(|entry| entry.path.as_ref() != *parent_path)
2895                        })
2896                        .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
2897                    if parent_expanded || query.is_some() {
2898                        outline_panel.push_entry(
2899                            &mut entries,
2900                            &mut match_candidates,
2901                            track_matches,
2902                            PanelEntry::FoldedDirs(worktree_id, folded_dirs),
2903                            folded_depth,
2904                            cx,
2905                        );
2906                    }
2907                }
2908            }) else {
2909                return Vec::new();
2910            };
2911
2912            let Some(query) = query else {
2913                return entries;
2914            };
2915            let mut matched_ids = match_strings(
2916                &match_candidates,
2917                &query,
2918                true,
2919                usize::MAX,
2920                &AtomicBool::default(),
2921                cx.background_executor().clone(),
2922            )
2923            .await
2924            .into_iter()
2925            .map(|string_match| (string_match.candidate_id, string_match))
2926            .collect::<HashMap<_, _>>();
2927
2928            let mut id = 0;
2929            entries.retain_mut(|cached_entry| {
2930                let retain = match matched_ids.remove(&id) {
2931                    Some(string_match) => {
2932                        cached_entry.string_match = Some(string_match);
2933                        true
2934                    }
2935                    None => false,
2936                };
2937                id += 1;
2938                retain
2939            });
2940
2941            entries
2942        })
2943    }
2944
2945    fn push_entry(
2946        &self,
2947        entries: &mut Vec<CachedEntry>,
2948        match_candidates: &mut Vec<StringMatchCandidate>,
2949        track_matches: bool,
2950        entry: PanelEntry,
2951        depth: usize,
2952        cx: &mut WindowContext,
2953    ) {
2954        if track_matches {
2955            let id = entries.len();
2956            match &entry {
2957                PanelEntry::Fs(fs_entry) => {
2958                    if let Some(file_name) =
2959                        self.relative_path(fs_entry, cx).as_deref().map(file_name)
2960                    {
2961                        match_candidates.push(StringMatchCandidate {
2962                            id,
2963                            string: file_name.to_string(),
2964                            char_bag: file_name.chars().collect(),
2965                        });
2966                    }
2967                }
2968                PanelEntry::FoldedDirs(worktree_id, entries) => {
2969                    let dir_names = self.dir_names_string(entries, *worktree_id, cx);
2970                    {
2971                        match_candidates.push(StringMatchCandidate {
2972                            id,
2973                            string: dir_names.to_string(),
2974                            char_bag: dir_names.chars().collect(),
2975                        });
2976                    }
2977                }
2978                PanelEntry::Outline(outline_entry) => match outline_entry {
2979                    OutlineEntry::Outline(_, _, outline) => {
2980                        match_candidates.push(StringMatchCandidate {
2981                            id,
2982                            string: outline.text.clone(),
2983                            char_bag: outline.text.chars().collect(),
2984                        });
2985                    }
2986                    OutlineEntry::Excerpt(..) => {}
2987                },
2988                PanelEntry::Search(new_search_entry) => {
2989                    if let Some(search_data) = new_search_entry
2990                        .render_data
2991                        .as_ref()
2992                        .and_then(|data| data.get())
2993                    {
2994                        match_candidates.push(StringMatchCandidate {
2995                            id,
2996                            char_bag: search_data.context_text.chars().collect(),
2997                            string: search_data.context_text.clone(),
2998                        });
2999                    }
3000                }
3001            }
3002        }
3003        entries.push(CachedEntry {
3004            depth,
3005            entry,
3006            string_match: None,
3007        });
3008    }
3009
3010    fn dir_names_string(
3011        &self,
3012        entries: &[Entry],
3013        worktree_id: WorktreeId,
3014        cx: &AppContext,
3015    ) -> String {
3016        let dir_names_segment = entries
3017            .iter()
3018            .map(|entry| self.entry_name(&worktree_id, entry, cx))
3019            .collect::<PathBuf>();
3020        dir_names_segment.to_string_lossy().to_string()
3021    }
3022
3023    fn query(&self, cx: &AppContext) -> Option<String> {
3024        let query = self.filter_editor.read(cx).text(cx);
3025        if query.trim().is_empty() {
3026            None
3027        } else {
3028            Some(query)
3029        }
3030    }
3031
3032    fn is_expanded(&self, entry: &FsEntry) -> bool {
3033        let entry_to_check = match entry {
3034            FsEntry::ExternalFile(buffer_id, _) => CollapsedEntry::ExternalFile(*buffer_id),
3035            FsEntry::File(worktree_id, _, buffer_id, _) => {
3036                CollapsedEntry::File(*worktree_id, *buffer_id)
3037            }
3038            FsEntry::Directory(worktree_id, entry) => CollapsedEntry::Dir(*worktree_id, entry.id),
3039        };
3040        !self.collapsed_entries.contains(&entry_to_check)
3041    }
3042
3043    fn update_non_fs_items(&mut self, cx: &mut ViewContext<OutlinePanel>) {
3044        if !self.active {
3045            return;
3046        }
3047
3048        self.update_search_matches(cx);
3049        self.fetch_outdated_outlines(cx);
3050        self.autoscroll(cx);
3051    }
3052
3053    fn update_search_matches(&mut self, cx: &mut ViewContext<OutlinePanel>) {
3054        if !self.active {
3055            return;
3056        }
3057
3058        let active_editor = self.active_editor();
3059        let project_search = self.active_project_search(active_editor.as_ref(), cx);
3060        let project_search_matches = project_search
3061            .as_ref()
3062            .map(|project_search| project_search.read(cx).get_matches(cx))
3063            .unwrap_or_default();
3064
3065        let buffer_search = active_editor
3066            .as_ref()
3067            .and_then(|active_editor| self.workspace.read(cx).pane_for(active_editor))
3068            .and_then(|pane| {
3069                pane.read(cx)
3070                    .toolbar()
3071                    .read(cx)
3072                    .item_of_type::<BufferSearchBar>()
3073            });
3074        let buffer_search_matches = active_editor
3075            .map(|active_editor| active_editor.update(cx, |editor, cx| editor.get_matches(cx)))
3076            .unwrap_or_default();
3077
3078        let mut update_cached_entries = false;
3079        if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
3080            self.search_matches.clear();
3081            self.search = None;
3082            if self.mode == ItemsDisplayMode::Search {
3083                self.mode = ItemsDisplayMode::Outline;
3084                update_cached_entries = true;
3085            }
3086        } else {
3087            let new_search_matches = if buffer_search_matches.is_empty() {
3088                self.search = project_search.map(|project_search| {
3089                    (
3090                        SearchKind::Project,
3091                        project_search.read(cx).search_query_text(cx),
3092                    )
3093                });
3094                project_search_matches
3095            } else {
3096                self.search = buffer_search
3097                    .map(|buffer_search| (SearchKind::Buffer, buffer_search.read(cx).query(cx)));
3098                buffer_search_matches
3099            };
3100            update_cached_entries = self.mode != ItemsDisplayMode::Search
3101                || self.search_matches.is_empty()
3102                || self.search_matches != new_search_matches;
3103            self.search_matches = new_search_matches;
3104            self.mode = ItemsDisplayMode::Search;
3105        }
3106        if update_cached_entries {
3107            self.selected_entry.invalidate();
3108            self.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
3109        }
3110    }
3111
3112    fn active_project_search(
3113        &mut self,
3114        for_editor: Option<&View<Editor>>,
3115        cx: &mut ViewContext<Self>,
3116    ) -> Option<View<ProjectSearchView>> {
3117        let for_editor = for_editor?;
3118        self.workspace
3119            .read(cx)
3120            .active_pane()
3121            .read(cx)
3122            .items()
3123            .filter_map(|item| item.downcast::<ProjectSearchView>())
3124            .find(|project_search| {
3125                let project_search_editor = project_search.boxed_clone().act_as::<Editor>(cx);
3126                Some(for_editor) == project_search_editor.as_ref()
3127            })
3128    }
3129
3130    #[allow(clippy::too_many_arguments)]
3131    fn add_excerpt_entries(
3132        &self,
3133        buffer_id: BufferId,
3134        entries_to_add: &[ExcerptId],
3135        parent_depth: usize,
3136        track_matches: bool,
3137        is_singleton: bool,
3138        query: Option<&str>,
3139        entries: &mut Vec<CachedEntry>,
3140        match_candidates: &mut Vec<StringMatchCandidate>,
3141        cx: &mut ViewContext<Self>,
3142    ) {
3143        if let Some(excerpts) = self.excerpts.get(&buffer_id) {
3144            for &excerpt_id in entries_to_add {
3145                let Some(excerpt) = excerpts.get(&excerpt_id) else {
3146                    continue;
3147                };
3148                let excerpt_depth = parent_depth + 1;
3149                self.push_entry(
3150                    entries,
3151                    match_candidates,
3152                    track_matches,
3153                    PanelEntry::Outline(OutlineEntry::Excerpt(
3154                        buffer_id,
3155                        excerpt_id,
3156                        excerpt.range.clone(),
3157                    )),
3158                    excerpt_depth,
3159                    cx,
3160                );
3161
3162                let mut outline_base_depth = excerpt_depth + 1;
3163                if is_singleton {
3164                    outline_base_depth = 0;
3165                    entries.clear();
3166                    match_candidates.clear();
3167                } else if query.is_none()
3168                    && self
3169                        .collapsed_entries
3170                        .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
3171                {
3172                    continue;
3173                }
3174
3175                for outline in excerpt.iter_outlines() {
3176                    self.push_entry(
3177                        entries,
3178                        match_candidates,
3179                        track_matches,
3180                        PanelEntry::Outline(OutlineEntry::Outline(
3181                            buffer_id,
3182                            excerpt_id,
3183                            outline.clone(),
3184                        )),
3185                        outline_base_depth + outline.depth,
3186                        cx,
3187                    );
3188                }
3189            }
3190        }
3191    }
3192
3193    #[allow(clippy::too_many_arguments)]
3194    fn add_search_entries(
3195        &self,
3196        entry: &FsEntry,
3197        parent_depth: usize,
3198        track_matches: bool,
3199        is_singleton: bool,
3200        entries: &mut Vec<CachedEntry>,
3201        match_candidates: &mut Vec<StringMatchCandidate>,
3202        cx: &mut ViewContext<Self>,
3203    ) {
3204        let related_excerpts = match entry {
3205            FsEntry::Directory(_, _) => return,
3206            FsEntry::ExternalFile(_, excerpts) => excerpts,
3207            FsEntry::File(_, _, _, excerpts) => excerpts,
3208        }
3209        .iter()
3210        .copied()
3211        .collect::<HashSet<_>>();
3212        if related_excerpts.is_empty() || self.search_matches.is_empty() {
3213            return;
3214        }
3215        let Some(kind) = self.search.as_ref().map(|&(kind, _)| kind) else {
3216            return;
3217        };
3218
3219        for match_range in &self.search_matches {
3220            if related_excerpts.contains(&match_range.start.excerpt_id)
3221                || related_excerpts.contains(&match_range.end.excerpt_id)
3222            {
3223                let depth = if is_singleton { 0 } else { parent_depth + 1 };
3224                let previous_search_entry = entries.last_mut().and_then(|entry| {
3225                    if let PanelEntry::Search(previous_search_entry) = &mut entry.entry {
3226                        Some(previous_search_entry)
3227                    } else {
3228                        None
3229                    }
3230                });
3231                let mut new_search_entry = SearchEntry {
3232                    kind,
3233                    match_range: match_range.clone(),
3234                    same_line_matches: Vec::new(),
3235                    render_data: Some(OnceCell::new()),
3236                };
3237                if self.init_search_data(previous_search_entry, &mut new_search_entry, cx) {
3238                    self.push_entry(
3239                        entries,
3240                        match_candidates,
3241                        track_matches,
3242                        PanelEntry::Search(new_search_entry),
3243                        depth,
3244                        cx,
3245                    );
3246                }
3247            }
3248        }
3249    }
3250
3251    fn active_editor(&self) -> Option<View<Editor>> {
3252        self.active_item.as_ref()?.active_editor.upgrade()
3253    }
3254
3255    fn should_replace_active_editor(&self, new_active_editor: &View<Editor>) -> bool {
3256        self.active_editor().map_or(true, |active_editor| {
3257            !self.pinned && active_editor.item_id() != new_active_editor.item_id()
3258        })
3259    }
3260
3261    pub fn toggle_active_editor_pin(
3262        &mut self,
3263        _: &ToggleActiveEditorPin,
3264        cx: &mut ViewContext<Self>,
3265    ) {
3266        self.pinned = !self.pinned;
3267        if !self.pinned {
3268            if let Some(active_editor) = workspace_active_editor(self.workspace.read(cx), cx) {
3269                if self.should_replace_active_editor(&active_editor) {
3270                    self.replace_active_editor(active_editor, cx);
3271                }
3272            }
3273        }
3274
3275        cx.notify();
3276    }
3277
3278    fn selected_entry(&self) -> Option<&PanelEntry> {
3279        match &self.selected_entry {
3280            SelectedEntry::Invalidated(entry) => entry.as_ref(),
3281            SelectedEntry::Valid(entry) => Some(entry),
3282            SelectedEntry::None => None,
3283        }
3284    }
3285
3286    fn init_search_data(
3287        &self,
3288        previous_search_entry: Option<&mut SearchEntry>,
3289        new_search_entry: &mut SearchEntry,
3290        cx: &WindowContext,
3291    ) -> bool {
3292        let Some(active_editor) = self.active_editor() else {
3293            return false;
3294        };
3295        let multi_buffer_snapshot = active_editor.read(cx).buffer().read(cx).snapshot(cx);
3296        let theme = cx.theme().syntax().clone();
3297        let previous_search_data = previous_search_entry.and_then(|previous_search_entry| {
3298            let previous_search_data = previous_search_entry.render_data.as_mut()?;
3299            previous_search_data.get_or_init(|| {
3300                SearchData::new(
3301                    new_search_entry.kind,
3302                    &previous_search_entry.match_range,
3303                    &multi_buffer_snapshot,
3304                    &theme,
3305                )
3306            });
3307            previous_search_data.get_mut()
3308        });
3309        let new_search_data = new_search_entry.render_data.as_mut().and_then(|data| {
3310            data.get_or_init(|| {
3311                SearchData::new(
3312                    new_search_entry.kind,
3313                    &new_search_entry.match_range,
3314                    &multi_buffer_snapshot,
3315                    &theme,
3316                )
3317            });
3318            data.get_mut()
3319        });
3320        match (previous_search_data, new_search_data) {
3321            (_, None) => false,
3322            (None, Some(_)) => true,
3323            (Some(previous_search_data), Some(new_search_data)) => {
3324                if previous_search_data.context_range == new_search_data.context_range {
3325                    previous_search_data
3326                        .highlight_ranges
3327                        .append(&mut new_search_data.highlight_ranges);
3328                    previous_search_data
3329                        .search_match_indices
3330                        .append(&mut new_search_data.search_match_indices);
3331                    false
3332                } else {
3333                    true
3334                }
3335            }
3336        }
3337    }
3338
3339    fn select_entry(&mut self, entry: PanelEntry, focus: bool, cx: &mut ViewContext<Self>) {
3340        if focus {
3341            self.focus_handle.focus(cx);
3342        }
3343        self.selected_entry = SelectedEntry::Valid(entry);
3344        self.autoscroll(cx);
3345        cx.notify();
3346    }
3347}
3348
3349fn workspace_active_editor(workspace: &Workspace, cx: &AppContext) -> Option<View<Editor>> {
3350    workspace
3351        .active_item(cx)?
3352        .act_as::<Editor>(cx)
3353        .filter(|editor| editor.read(cx).mode() == EditorMode::Full)
3354}
3355
3356fn back_to_common_visited_parent(
3357    visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
3358    worktree_id: &WorktreeId,
3359    new_entry: &Entry,
3360) -> Option<(WorktreeId, ProjectEntryId)> {
3361    while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
3362        match new_entry.path.parent() {
3363            Some(parent_path) => {
3364                if parent_path == visited_path.as_ref() {
3365                    return Some((*worktree_id, *visited_dir_id));
3366                }
3367            }
3368            None => {
3369                break;
3370            }
3371        }
3372        visited_dirs.pop();
3373    }
3374    None
3375}
3376
3377fn file_name(path: &Path) -> String {
3378    let mut current_path = path;
3379    loop {
3380        if let Some(file_name) = current_path.file_name() {
3381            return file_name.to_string_lossy().into_owned();
3382        }
3383        match current_path.parent() {
3384            Some(parent) => current_path = parent,
3385            None => return path.to_string_lossy().into_owned(),
3386        }
3387    }
3388}
3389
3390impl Panel for OutlinePanel {
3391    fn persistent_name() -> &'static str {
3392        "Outline Panel"
3393    }
3394
3395    fn position(&self, cx: &WindowContext) -> DockPosition {
3396        match OutlinePanelSettings::get_global(cx).dock {
3397            OutlinePanelDockPosition::Left => DockPosition::Left,
3398            OutlinePanelDockPosition::Right => DockPosition::Right,
3399        }
3400    }
3401
3402    fn position_is_valid(&self, position: DockPosition) -> bool {
3403        matches!(position, DockPosition::Left | DockPosition::Right)
3404    }
3405
3406    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3407        settings::update_settings_file::<OutlinePanelSettings>(
3408            self.fs.clone(),
3409            cx,
3410            move |settings, _| {
3411                let dock = match position {
3412                    DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
3413                    DockPosition::Right => OutlinePanelDockPosition::Right,
3414                };
3415                settings.dock = Some(dock);
3416            },
3417        );
3418    }
3419
3420    fn size(&self, cx: &WindowContext) -> Pixels {
3421        self.width
3422            .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
3423    }
3424
3425    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
3426        self.width = size;
3427        self.serialize(cx);
3428        cx.notify();
3429    }
3430
3431    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
3432        OutlinePanelSettings::get_global(cx)
3433            .button
3434            .then(|| IconName::ListTree)
3435    }
3436
3437    fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
3438        Some("Outline Panel")
3439    }
3440
3441    fn toggle_action(&self) -> Box<dyn Action> {
3442        Box::new(ToggleFocus)
3443    }
3444
3445    fn starts_open(&self, _: &WindowContext) -> bool {
3446        self.active
3447    }
3448
3449    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
3450        cx.spawn(|outline_panel, mut cx| async move {
3451            outline_panel
3452                .update(&mut cx, |outline_panel, cx| {
3453                    let old_active = outline_panel.active;
3454                    outline_panel.active = active;
3455                    if active && old_active != active {
3456                        if let Some(active_editor) =
3457                            workspace_active_editor(outline_panel.workspace.read(cx), cx)
3458                        {
3459                            if outline_panel.should_replace_active_editor(&active_editor) {
3460                                outline_panel.replace_active_editor(active_editor, cx);
3461                            } else {
3462                                outline_panel.update_fs_entries(
3463                                    &active_editor,
3464                                    HashSet::default(),
3465                                    None,
3466                                    cx,
3467                                )
3468                            }
3469                        } else if !outline_panel.pinned {
3470                            outline_panel.clear_previous(cx);
3471                        }
3472                    }
3473                    outline_panel.serialize(cx);
3474                })
3475                .ok();
3476        })
3477        .detach()
3478    }
3479}
3480
3481impl FocusableView for OutlinePanel {
3482    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3483        self.filter_editor.focus_handle(cx).clone()
3484    }
3485}
3486
3487impl EventEmitter<Event> for OutlinePanel {}
3488
3489impl EventEmitter<PanelEvent> for OutlinePanel {}
3490
3491impl Render for OutlinePanel {
3492    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3493        let project = self.project.read(cx);
3494        let query = self.query(cx);
3495        let pinned = self.pinned;
3496
3497        let outline_panel = v_flex()
3498            .id("outline-panel")
3499            .size_full()
3500            .relative()
3501            .key_context(self.dispatch_context(cx))
3502            .on_action(cx.listener(Self::open))
3503            .on_action(cx.listener(Self::cancel))
3504            .on_action(cx.listener(Self::select_next))
3505            .on_action(cx.listener(Self::select_prev))
3506            .on_action(cx.listener(Self::select_first))
3507            .on_action(cx.listener(Self::select_last))
3508            .on_action(cx.listener(Self::select_parent))
3509            .on_action(cx.listener(Self::expand_selected_entry))
3510            .on_action(cx.listener(Self::collapse_selected_entry))
3511            .on_action(cx.listener(Self::expand_all_entries))
3512            .on_action(cx.listener(Self::collapse_all_entries))
3513            .on_action(cx.listener(Self::copy_path))
3514            .on_action(cx.listener(Self::copy_relative_path))
3515            .on_action(cx.listener(Self::toggle_active_editor_pin))
3516            .on_action(cx.listener(Self::unfold_directory))
3517            .on_action(cx.listener(Self::fold_directory))
3518            .when(project.is_local_or_ssh(), |el| {
3519                el.on_action(cx.listener(Self::reveal_in_finder))
3520                    .on_action(cx.listener(Self::open_in_terminal))
3521            })
3522            .on_mouse_down(
3523                MouseButton::Right,
3524                cx.listener(move |outline_panel, event: &MouseDownEvent, cx| {
3525                    if let Some(entry) = outline_panel.selected_entry().cloned() {
3526                        outline_panel.deploy_context_menu(event.position, entry, cx)
3527                    } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
3528                        outline_panel.deploy_context_menu(event.position, PanelEntry::Fs(entry), cx)
3529                    }
3530                }),
3531            )
3532            .track_focus(&self.focus_handle);
3533
3534        if self.cached_entries.is_empty() {
3535            let header = if self.updating_fs_entries {
3536                "Loading outlines"
3537            } else if query.is_some() {
3538                "No matches for query"
3539            } else {
3540                "No outlines available"
3541            };
3542
3543            outline_panel.child(
3544                v_flex()
3545                    .justify_center()
3546                    .size_full()
3547                    .child(h_flex().justify_center().child(Label::new(header)))
3548                    .when_some(query.clone(), |panel, query| {
3549                        panel.child(h_flex().justify_center().child(Label::new(query)))
3550                    })
3551                    .child(
3552                        h_flex()
3553                            .pt(Spacing::Small.rems(cx))
3554                            .justify_center()
3555                            .child({
3556                                let keystroke = match self.position(cx) {
3557                                    DockPosition::Left => {
3558                                        cx.keystroke_text_for(&workspace::ToggleLeftDock)
3559                                    }
3560                                    DockPosition::Bottom => {
3561                                        cx.keystroke_text_for(&workspace::ToggleBottomDock)
3562                                    }
3563                                    DockPosition::Right => {
3564                                        cx.keystroke_text_for(&workspace::ToggleRightDock)
3565                                    }
3566                                };
3567                                Label::new(format!("Toggle this panel with {keystroke}"))
3568                            }),
3569                    ),
3570            )
3571        } else {
3572            outline_panel
3573                .when_some(self.search.as_ref(), |outline_panel, (_, search_query)| {
3574                    outline_panel.child(
3575                        div()
3576                            .mx_2()
3577                            .child(
3578                                Label::new(format!("Searching: '{search_query}'"))
3579                                    .color(Color::Muted),
3580                            )
3581                            .child(horizontal_separator(cx)),
3582                    )
3583                })
3584                .child({
3585                    let items_len = self.cached_entries.len();
3586                    uniform_list(cx.view().clone(), "entries", items_len, {
3587                        move |outline_panel, range, cx| {
3588                            let entries = outline_panel.cached_entries.get(range);
3589                            entries
3590                                .map(|entries| entries.to_vec())
3591                                .unwrap_or_default()
3592                                .into_iter()
3593                                .filter_map(|cached_entry| match cached_entry.entry {
3594                                    PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
3595                                        &entry,
3596                                        cached_entry.depth,
3597                                        cached_entry.string_match.as_ref(),
3598                                        cx,
3599                                    )),
3600                                    PanelEntry::FoldedDirs(worktree_id, entries) => {
3601                                        Some(outline_panel.render_folded_dirs(
3602                                            worktree_id,
3603                                            &entries,
3604                                            cached_entry.depth,
3605                                            cached_entry.string_match.as_ref(),
3606                                            cx,
3607                                        ))
3608                                    }
3609                                    PanelEntry::Outline(OutlineEntry::Excerpt(
3610                                        buffer_id,
3611                                        excerpt_id,
3612                                        excerpt,
3613                                    )) => outline_panel.render_excerpt(
3614                                        buffer_id,
3615                                        excerpt_id,
3616                                        &excerpt,
3617                                        cached_entry.depth,
3618                                        cx,
3619                                    ),
3620                                    PanelEntry::Outline(OutlineEntry::Outline(
3621                                        buffer_id,
3622                                        excerpt_id,
3623                                        outline,
3624                                    )) => Some(outline_panel.render_outline(
3625                                        buffer_id,
3626                                        excerpt_id,
3627                                        &outline,
3628                                        cached_entry.depth,
3629                                        cached_entry.string_match.as_ref(),
3630                                        cx,
3631                                    )),
3632                                    PanelEntry::Search(SearchEntry {
3633                                        match_range,
3634                                        render_data,
3635                                        kind,
3636                                        same_line_matches: _,
3637                                    }) => render_data.as_ref().and_then(|search_data| {
3638                                        let search_data = search_data.get()?;
3639                                        Some(outline_panel.render_search_match(
3640                                            &match_range,
3641                                            search_data,
3642                                            kind,
3643                                            cached_entry.depth,
3644                                            cached_entry.string_match.as_ref(),
3645                                            cx,
3646                                        ))
3647                                    }),
3648                                })
3649                                .collect()
3650                        }
3651                    })
3652                    .size_full()
3653                    .track_scroll(self.scroll_handle.clone())
3654                })
3655        }
3656        .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3657            deferred(
3658                anchored()
3659                    .position(*position)
3660                    .anchor(gpui::AnchorCorner::TopLeft)
3661                    .child(menu.clone()),
3662            )
3663            .with_priority(1)
3664        }))
3665        .child(
3666            v_flex().child(horizontal_separator(cx)).child(
3667                h_flex().p_2().child(self.filter_editor.clone()).child(
3668                    div().border_1().child(
3669                        IconButton::new(
3670                            "outline-panel-menu",
3671                            if pinned {
3672                                IconName::Unpin
3673                            } else {
3674                                IconName::Pin
3675                            },
3676                        )
3677                        .tooltip(move |cx| {
3678                            Tooltip::text(if pinned { "Unpin" } else { "Pin active editor" }, cx)
3679                        })
3680                        .shape(IconButtonShape::Square)
3681                        .on_click(cx.listener(|outline_panel, _, cx| {
3682                            outline_panel.toggle_active_editor_pin(&ToggleActiveEditorPin, cx);
3683                        })),
3684                    ),
3685                ),
3686            ),
3687        )
3688    }
3689}
3690
3691fn subscribe_for_editor_events(
3692    editor: &View<Editor>,
3693    cx: &mut ViewContext<OutlinePanel>,
3694) -> Subscription {
3695    let debounce = Some(UPDATE_DEBOUNCE);
3696    cx.subscribe(
3697        editor,
3698        move |outline_panel, editor, e: &EditorEvent, cx| match e {
3699            EditorEvent::SelectionsChanged { local: true } => {
3700                outline_panel.reveal_entry_for_selection(&editor, cx);
3701                cx.notify();
3702            }
3703            EditorEvent::ExcerptsAdded { excerpts, .. } => {
3704                outline_panel.update_fs_entries(
3705                    &editor,
3706                    excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(),
3707                    debounce,
3708                    cx,
3709                );
3710            }
3711            EditorEvent::ExcerptsRemoved { ids } => {
3712                let mut ids = ids.into_iter().collect::<HashSet<_>>();
3713                for excerpts in outline_panel.excerpts.values_mut() {
3714                    excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
3715                    if ids.is_empty() {
3716                        break;
3717                    }
3718                }
3719                outline_panel.update_fs_entries(&editor, HashSet::default(), debounce, cx);
3720            }
3721            EditorEvent::ExcerptsExpanded { ids } => {
3722                outline_panel.invalidate_outlines(ids);
3723                outline_panel.update_non_fs_items(cx);
3724            }
3725            EditorEvent::ExcerptsEdited { ids } => {
3726                outline_panel.invalidate_outlines(ids);
3727                outline_panel.update_non_fs_items(cx);
3728            }
3729            EditorEvent::Reparsed(buffer_id) => {
3730                if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
3731                    for (_, excerpt) in excerpts {
3732                        excerpt.invalidate_outlines();
3733                    }
3734                }
3735                outline_panel.update_non_fs_items(cx);
3736            }
3737            _ => {}
3738        },
3739    )
3740}
3741
3742fn empty_icon() -> AnyElement {
3743    h_flex()
3744        .size(IconSize::default().rems())
3745        .invisible()
3746        .flex_none()
3747        .into_any_element()
3748}
3749
3750fn horizontal_separator(cx: &mut WindowContext) -> Div {
3751    div().mx_2().border_primary(cx).border_t_1()
3752}