outline_panel.rs

   1mod outline_panel_settings;
   2
   3use anyhow::Context as _;
   4use collections::{BTreeSet, HashMap, HashSet, hash_map};
   5use db::kvp::KEY_VALUE_STORE;
   6use editor::{
   7    AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorSettings, ExcerptId,
   8    ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects, ShowScrollbar,
   9    display_map::ToDisplayPoint,
  10    items::{entry_git_aware_label_color, entry_label_color},
  11    scroll::{Autoscroll, ScrollAnchor, ScrollbarAutoHide},
  12};
  13use file_icons::FileIcons;
  14use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
  15use gpui::{
  16    Action, AnyElement, App, AppContext as _, AsyncWindowContext, Bounds, ClipboardItem, Context,
  17    DismissEvent, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
  18    InteractiveElement, IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
  19    MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy,
  20    SharedString, Stateful, StatefulInteractiveElement as _, Styled, Subscription, Task,
  21    UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, point, px, size,
  22    uniform_list,
  23};
  24use itertools::Itertools;
  25use language::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
  26use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious};
  27use std::{
  28    cmp,
  29    collections::BTreeMap,
  30    hash::Hash,
  31    ops::Range,
  32    path::{MAIN_SEPARATOR_STR, Path, PathBuf},
  33    sync::{
  34        Arc, OnceLock,
  35        atomic::{self, AtomicBool},
  36    },
  37    time::Duration,
  38    u32,
  39};
  40
  41use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings, ShowIndentGuides};
  42use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem};
  43use search::{BufferSearchBar, ProjectSearchView};
  44use serde::{Deserialize, Serialize};
  45use settings::{Settings, SettingsStore};
  46use smol::channel;
  47use theme::{SyntaxTheme, ThemeSettings};
  48use ui::{DynamicSpacing, IndentGuideColors, IndentGuideLayout};
  49use util::{RangeExt, ResultExt, TryFutureExt, debug_panic};
  50use workspace::{
  51    OpenInTerminal, WeakItemHandle, Workspace,
  52    dock::{DockPosition, Panel, PanelEvent},
  53    item::ItemHandle,
  54    searchable::{SearchEvent, SearchableItem},
  55    ui::{
  56        ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder, HighlightedLabel,
  57        Icon, IconButton, IconButtonShape, IconName, IconSize, Label, LabelCommon, ListItem,
  58        Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable, Tooltip, h_flex,
  59        v_flex,
  60    },
  61};
  62use worktree::{Entry, ProjectEntryId, WorktreeId};
  63
  64actions!(
  65    outline_panel,
  66    [
  67        /// Collapses all entries in the outline tree.
  68        CollapseAllEntries,
  69        /// Collapses the currently selected entry.
  70        CollapseSelectedEntry,
  71        /// Expands all entries in the outline tree.
  72        ExpandAllEntries,
  73        /// Expands the currently selected entry.
  74        ExpandSelectedEntry,
  75        /// Folds the selected directory.
  76        FoldDirectory,
  77        /// Opens the selected entry in the editor.
  78        OpenSelectedEntry,
  79        /// Reveals the selected item in the system file manager.
  80        RevealInFileManager,
  81        /// Selects the parent of the current entry.
  82        SelectParent,
  83        /// Toggles the pin status of the active editor.
  84        ToggleActiveEditorPin,
  85        /// Unfolds the selected directory.
  86        UnfoldDirectory,
  87        /// Toggles focus on the outline panel.
  88        ToggleFocus,
  89    ]
  90);
  91
  92const OUTLINE_PANEL_KEY: &str = "OutlinePanel";
  93const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
  94
  95type Outline = OutlineItem<language::Anchor>;
  96type HighlightStyleData = Arc<OnceLock<Vec<(Range<usize>, HighlightStyle)>>>;
  97
  98pub struct OutlinePanel {
  99    fs: Arc<dyn Fs>,
 100    width: Option<Pixels>,
 101    project: Entity<Project>,
 102    workspace: WeakEntity<Workspace>,
 103    active: bool,
 104    pinned: bool,
 105    scroll_handle: UniformListScrollHandle,
 106    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
 107    focus_handle: FocusHandle,
 108    pending_serialization: Task<Option<()>>,
 109    fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,
 110    fs_entries: Vec<FsEntry>,
 111    fs_children_count: HashMap<WorktreeId, HashMap<Arc<Path>, FsChildren>>,
 112    collapsed_entries: HashSet<CollapsedEntry>,
 113    unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
 114    selected_entry: SelectedEntry,
 115    active_item: Option<ActiveItem>,
 116    _subscriptions: Vec<Subscription>,
 117    updating_fs_entries: bool,
 118    updating_cached_entries: bool,
 119    new_entries_for_fs_update: HashSet<ExcerptId>,
 120    fs_entries_update_task: Task<()>,
 121    cached_entries_update_task: Task<()>,
 122    reveal_selection_task: Task<anyhow::Result<()>>,
 123    outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>,
 124    excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
 125    cached_entries: Vec<CachedEntry>,
 126    filter_editor: Entity<Editor>,
 127    mode: ItemsDisplayMode,
 128    show_scrollbar: bool,
 129    vertical_scrollbar_state: ScrollbarState,
 130    horizontal_scrollbar_state: ScrollbarState,
 131    hide_scrollbar_task: Option<Task<()>>,
 132    max_width_item_index: Option<usize>,
 133    preserve_selection_on_buffer_fold_toggles: HashSet<BufferId>,
 134    pending_default_expansion_depth: Option<usize>,
 135    outline_children_cache: HashMap<BufferId, HashMap<(Range<Anchor>, usize), bool>>,
 136}
 137
 138#[derive(Debug)]
 139enum ItemsDisplayMode {
 140    Search(SearchState),
 141    Outline,
 142}
 143
 144#[derive(Debug)]
 145struct SearchState {
 146    kind: SearchKind,
 147    query: String,
 148    matches: Vec<(Range<editor::Anchor>, Arc<OnceLock<SearchData>>)>,
 149    highlight_search_match_tx: channel::Sender<HighlightArguments>,
 150    _search_match_highlighter: Task<()>,
 151    _search_match_notify: Task<()>,
 152}
 153
 154struct HighlightArguments {
 155    multi_buffer_snapshot: MultiBufferSnapshot,
 156    match_range: Range<editor::Anchor>,
 157    search_data: Arc<OnceLock<SearchData>>,
 158}
 159
 160impl SearchState {
 161    fn new(
 162        kind: SearchKind,
 163        query: String,
 164        previous_matches: HashMap<Range<editor::Anchor>, Arc<OnceLock<SearchData>>>,
 165        new_matches: Vec<Range<editor::Anchor>>,
 166        theme: Arc<SyntaxTheme>,
 167        window: &mut Window,
 168        cx: &mut Context<OutlinePanel>,
 169    ) -> Self {
 170        let (highlight_search_match_tx, highlight_search_match_rx) = channel::unbounded();
 171        let (notify_tx, notify_rx) = channel::unbounded::<()>();
 172        Self {
 173            kind,
 174            query,
 175            matches: new_matches
 176                .into_iter()
 177                .map(|range| {
 178                    let search_data = previous_matches
 179                        .get(&range)
 180                        .map(Arc::clone)
 181                        .unwrap_or_default();
 182                    (range, search_data)
 183                })
 184                .collect(),
 185            highlight_search_match_tx,
 186            _search_match_highlighter: cx.background_spawn(async move {
 187                while let Ok(highlight_arguments) = highlight_search_match_rx.recv().await {
 188                    let needs_init = highlight_arguments.search_data.get().is_none();
 189                    let search_data = highlight_arguments.search_data.get_or_init(|| {
 190                        SearchData::new(
 191                            &highlight_arguments.match_range,
 192                            &highlight_arguments.multi_buffer_snapshot,
 193                        )
 194                    });
 195                    if needs_init {
 196                        notify_tx.try_send(()).ok();
 197                    }
 198
 199                    let highlight_data = &search_data.highlights_data;
 200                    if highlight_data.get().is_some() {
 201                        continue;
 202                    }
 203                    let mut left_whitespaces_count = 0;
 204                    let mut non_whitespace_symbol_occurred = false;
 205                    let context_offset_range = search_data
 206                        .context_range
 207                        .to_offset(&highlight_arguments.multi_buffer_snapshot);
 208                    let mut offset = context_offset_range.start;
 209                    let mut context_text = String::new();
 210                    let mut highlight_ranges = Vec::new();
 211                    for mut chunk in highlight_arguments
 212                        .multi_buffer_snapshot
 213                        .chunks(context_offset_range.start..context_offset_range.end, true)
 214                    {
 215                        if !non_whitespace_symbol_occurred {
 216                            for c in chunk.text.chars() {
 217                                if c.is_whitespace() {
 218                                    left_whitespaces_count += c.len_utf8();
 219                                } else {
 220                                    non_whitespace_symbol_occurred = true;
 221                                    break;
 222                                }
 223                            }
 224                        }
 225
 226                        if chunk.text.len() > context_offset_range.end - offset {
 227                            chunk.text = &chunk.text[0..(context_offset_range.end - offset)];
 228                            offset = context_offset_range.end;
 229                        } else {
 230                            offset += chunk.text.len();
 231                        }
 232                        let style = chunk
 233                            .syntax_highlight_id
 234                            .and_then(|highlight| highlight.style(&theme));
 235                        if let Some(style) = style {
 236                            let start = context_text.len();
 237                            let end = start + chunk.text.len();
 238                            highlight_ranges.push((start..end, style));
 239                        }
 240                        context_text.push_str(chunk.text);
 241                        if offset >= context_offset_range.end {
 242                            break;
 243                        }
 244                    }
 245
 246                    highlight_ranges.iter_mut().for_each(|(range, _)| {
 247                        range.start = range.start.saturating_sub(left_whitespaces_count);
 248                        range.end = range.end.saturating_sub(left_whitespaces_count);
 249                    });
 250                    if highlight_data.set(highlight_ranges).ok().is_some() {
 251                        notify_tx.try_send(()).ok();
 252                    }
 253
 254                    let trimmed_text = context_text[left_whitespaces_count..].to_owned();
 255                    debug_assert_eq!(
 256                        trimmed_text, search_data.context_text,
 257                        "Highlighted text that does not match the buffer text"
 258                    );
 259                }
 260            }),
 261            _search_match_notify: cx.spawn_in(window, async move |outline_panel, cx| {
 262                loop {
 263                    match notify_rx.recv().await {
 264                        Ok(()) => {}
 265                        Err(_) => break,
 266                    };
 267                    while let Ok(()) = notify_rx.try_recv() {
 268                        //
 269                    }
 270                    let update_result = outline_panel.update(cx, |_, cx| {
 271                        cx.notify();
 272                    });
 273                    if update_result.is_err() {
 274                        break;
 275                    }
 276                }
 277            }),
 278        }
 279    }
 280}
 281
 282#[derive(Debug)]
 283enum SelectedEntry {
 284    Invalidated(Option<PanelEntry>),
 285    Valid(PanelEntry, usize),
 286    None,
 287}
 288
 289impl SelectedEntry {
 290    fn invalidate(&mut self) {
 291        match std::mem::replace(self, SelectedEntry::None) {
 292            Self::Valid(entry, _) => *self = Self::Invalidated(Some(entry)),
 293            Self::None => *self = Self::Invalidated(None),
 294            other => *self = other,
 295        }
 296    }
 297
 298    fn is_invalidated(&self) -> bool {
 299        matches!(self, Self::Invalidated(_))
 300    }
 301}
 302
 303#[derive(Debug, Clone, Copy, Default)]
 304struct FsChildren {
 305    files: usize,
 306    dirs: usize,
 307}
 308
 309impl FsChildren {
 310    fn may_be_fold_part(&self) -> bool {
 311        self.dirs == 0 || (self.dirs == 1 && self.files == 0)
 312    }
 313}
 314
 315#[derive(Clone, Debug)]
 316struct CachedEntry {
 317    depth: usize,
 318    string_match: Option<StringMatch>,
 319    entry: PanelEntry,
 320}
 321
 322#[derive(Clone, Debug, PartialEq, Eq, Hash)]
 323enum CollapsedEntry {
 324    Dir(WorktreeId, ProjectEntryId),
 325    File(WorktreeId, BufferId),
 326    ExternalFile(BufferId),
 327    Excerpt(BufferId, ExcerptId),
 328    Outline(BufferId, ExcerptId, Range<Anchor>),
 329}
 330
 331#[derive(Debug)]
 332struct Excerpt {
 333    range: ExcerptRange<language::Anchor>,
 334    outlines: ExcerptOutlines,
 335}
 336
 337impl Excerpt {
 338    fn invalidate_outlines(&mut self) {
 339        if let ExcerptOutlines::Outlines(valid_outlines) = &mut self.outlines {
 340            self.outlines = ExcerptOutlines::Invalidated(std::mem::take(valid_outlines));
 341        }
 342    }
 343
 344    fn iter_outlines(&self) -> impl Iterator<Item = &Outline> {
 345        match &self.outlines {
 346            ExcerptOutlines::Outlines(outlines) => outlines.iter(),
 347            ExcerptOutlines::Invalidated(outlines) => outlines.iter(),
 348            ExcerptOutlines::NotFetched => [].iter(),
 349        }
 350    }
 351
 352    fn should_fetch_outlines(&self) -> bool {
 353        match &self.outlines {
 354            ExcerptOutlines::Outlines(_) => false,
 355            ExcerptOutlines::Invalidated(_) => true,
 356            ExcerptOutlines::NotFetched => true,
 357        }
 358    }
 359}
 360
 361#[derive(Debug)]
 362enum ExcerptOutlines {
 363    Outlines(Vec<Outline>),
 364    Invalidated(Vec<Outline>),
 365    NotFetched,
 366}
 367
 368#[derive(Clone, Debug, PartialEq, Eq)]
 369struct FoldedDirsEntry {
 370    worktree_id: WorktreeId,
 371    entries: Vec<GitEntry>,
 372}
 373
 374// TODO: collapse the inner enums into panel entry
 375#[derive(Clone, Debug)]
 376enum PanelEntry {
 377    Fs(FsEntry),
 378    FoldedDirs(FoldedDirsEntry),
 379    Outline(OutlineEntry),
 380    Search(SearchEntry),
 381}
 382
 383#[derive(Clone, Debug)]
 384struct SearchEntry {
 385    match_range: Range<editor::Anchor>,
 386    kind: SearchKind,
 387    render_data: Arc<OnceLock<SearchData>>,
 388}
 389
 390#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 391enum SearchKind {
 392    Project,
 393    Buffer,
 394}
 395
 396#[derive(Clone, Debug)]
 397struct SearchData {
 398    context_range: Range<editor::Anchor>,
 399    context_text: String,
 400    truncated_left: bool,
 401    truncated_right: bool,
 402    search_match_indices: Vec<Range<usize>>,
 403    highlights_data: HighlightStyleData,
 404}
 405
 406impl PartialEq for PanelEntry {
 407    fn eq(&self, other: &Self) -> bool {
 408        match (self, other) {
 409            (Self::Fs(a), Self::Fs(b)) => a == b,
 410            (
 411                Self::FoldedDirs(FoldedDirsEntry {
 412                    worktree_id: worktree_id_a,
 413                    entries: entries_a,
 414                }),
 415                Self::FoldedDirs(FoldedDirsEntry {
 416                    worktree_id: worktree_id_b,
 417                    entries: entries_b,
 418                }),
 419            ) => worktree_id_a == worktree_id_b && entries_a == entries_b,
 420            (Self::Outline(a), Self::Outline(b)) => a == b,
 421            (
 422                Self::Search(SearchEntry {
 423                    match_range: match_range_a,
 424                    kind: kind_a,
 425                    ..
 426                }),
 427                Self::Search(SearchEntry {
 428                    match_range: match_range_b,
 429                    kind: kind_b,
 430                    ..
 431                }),
 432            ) => match_range_a == match_range_b && kind_a == kind_b,
 433            _ => false,
 434        }
 435    }
 436}
 437
 438impl Eq for PanelEntry {}
 439
 440const SEARCH_MATCH_CONTEXT_SIZE: u32 = 40;
 441const TRUNCATED_CONTEXT_MARK: &str = "";
 442
 443impl SearchData {
 444    fn new(
 445        match_range: &Range<editor::Anchor>,
 446        multi_buffer_snapshot: &MultiBufferSnapshot,
 447    ) -> Self {
 448        let match_point_range = match_range.to_point(multi_buffer_snapshot);
 449        let context_left_border = multi_buffer_snapshot.clip_point(
 450            language::Point::new(
 451                match_point_range.start.row,
 452                match_point_range
 453                    .start
 454                    .column
 455                    .saturating_sub(SEARCH_MATCH_CONTEXT_SIZE),
 456            ),
 457            Bias::Left,
 458        );
 459        let context_right_border = multi_buffer_snapshot.clip_point(
 460            language::Point::new(
 461                match_point_range.end.row,
 462                match_point_range.end.column + SEARCH_MATCH_CONTEXT_SIZE,
 463            ),
 464            Bias::Right,
 465        );
 466
 467        let context_anchor_range =
 468            (context_left_border..context_right_border).to_anchors(multi_buffer_snapshot);
 469        let context_offset_range = context_anchor_range.to_offset(multi_buffer_snapshot);
 470        let match_offset_range = match_range.to_offset(multi_buffer_snapshot);
 471
 472        let mut search_match_indices = vec![
 473            multi_buffer_snapshot.clip_offset(
 474                match_offset_range.start - context_offset_range.start,
 475                Bias::Left,
 476            )
 477                ..multi_buffer_snapshot.clip_offset(
 478                    match_offset_range.end - context_offset_range.start,
 479                    Bias::Right,
 480                ),
 481        ];
 482
 483        let entire_context_text = multi_buffer_snapshot
 484            .text_for_range(context_offset_range.clone())
 485            .collect::<String>();
 486        let left_whitespaces_offset = entire_context_text
 487            .chars()
 488            .take_while(|c| c.is_whitespace())
 489            .map(|c| c.len_utf8())
 490            .sum::<usize>();
 491
 492        let mut extended_context_left_border = context_left_border;
 493        extended_context_left_border.column = extended_context_left_border.column.saturating_sub(1);
 494        let extended_context_left_border =
 495            multi_buffer_snapshot.clip_point(extended_context_left_border, Bias::Left);
 496        let mut extended_context_right_border = context_right_border;
 497        extended_context_right_border.column += 1;
 498        let extended_context_right_border =
 499            multi_buffer_snapshot.clip_point(extended_context_right_border, Bias::Right);
 500
 501        let truncated_left = left_whitespaces_offset == 0
 502            && extended_context_left_border < context_left_border
 503            && multi_buffer_snapshot
 504                .chars_at(extended_context_left_border)
 505                .last()
 506                .is_some_and(|c| !c.is_whitespace());
 507        let truncated_right = entire_context_text
 508            .chars()
 509            .last()
 510            .is_none_or(|c| !c.is_whitespace())
 511            && extended_context_right_border > context_right_border
 512            && multi_buffer_snapshot
 513                .chars_at(extended_context_right_border)
 514                .next()
 515                .is_some_and(|c| !c.is_whitespace());
 516        search_match_indices.iter_mut().for_each(|range| {
 517            range.start = multi_buffer_snapshot.clip_offset(
 518                range.start.saturating_sub(left_whitespaces_offset),
 519                Bias::Left,
 520            );
 521            range.end = multi_buffer_snapshot.clip_offset(
 522                range.end.saturating_sub(left_whitespaces_offset),
 523                Bias::Right,
 524            );
 525        });
 526
 527        let trimmed_row_offset_range =
 528            context_offset_range.start + left_whitespaces_offset..context_offset_range.end;
 529        let trimmed_text = entire_context_text[left_whitespaces_offset..].to_owned();
 530        Self {
 531            highlights_data: Arc::default(),
 532            search_match_indices,
 533            context_range: trimmed_row_offset_range.to_anchors(multi_buffer_snapshot),
 534            context_text: trimmed_text,
 535            truncated_left,
 536            truncated_right,
 537        }
 538    }
 539}
 540
 541#[derive(Clone, Debug, PartialEq, Eq, Hash)]
 542struct OutlineEntryExcerpt {
 543    id: ExcerptId,
 544    buffer_id: BufferId,
 545    range: ExcerptRange<language::Anchor>,
 546}
 547
 548#[derive(Clone, Debug, Eq)]
 549struct OutlineEntryOutline {
 550    buffer_id: BufferId,
 551    excerpt_id: ExcerptId,
 552    outline: Outline,
 553}
 554
 555impl PartialEq for OutlineEntryOutline {
 556    fn eq(&self, other: &Self) -> bool {
 557        self.buffer_id == other.buffer_id
 558            && self.excerpt_id == other.excerpt_id
 559            && self.outline.depth == other.outline.depth
 560            && self.outline.range == other.outline.range
 561            && self.outline.text == other.outline.text
 562    }
 563}
 564
 565impl Hash for OutlineEntryOutline {
 566    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
 567        (
 568            self.buffer_id,
 569            self.excerpt_id,
 570            self.outline.depth,
 571            &self.outline.range,
 572            &self.outline.text,
 573        )
 574            .hash(state);
 575    }
 576}
 577
 578#[derive(Clone, Debug, PartialEq, Eq)]
 579enum OutlineEntry {
 580    Excerpt(OutlineEntryExcerpt),
 581    Outline(OutlineEntryOutline),
 582}
 583
 584impl OutlineEntry {
 585    fn ids(&self) -> (BufferId, ExcerptId) {
 586        match self {
 587            OutlineEntry::Excerpt(excerpt) => (excerpt.buffer_id, excerpt.id),
 588            OutlineEntry::Outline(outline) => (outline.buffer_id, outline.excerpt_id),
 589        }
 590    }
 591}
 592
 593#[derive(Debug, Clone, Eq)]
 594struct FsEntryFile {
 595    worktree_id: WorktreeId,
 596    entry: GitEntry,
 597    buffer_id: BufferId,
 598    excerpts: Vec<ExcerptId>,
 599}
 600
 601impl PartialEq for FsEntryFile {
 602    fn eq(&self, other: &Self) -> bool {
 603        self.worktree_id == other.worktree_id
 604            && self.entry.id == other.entry.id
 605            && self.buffer_id == other.buffer_id
 606    }
 607}
 608
 609impl Hash for FsEntryFile {
 610    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
 611        (self.buffer_id, self.entry.id, self.worktree_id).hash(state);
 612    }
 613}
 614
 615#[derive(Debug, Clone, Eq)]
 616struct FsEntryDirectory {
 617    worktree_id: WorktreeId,
 618    entry: GitEntry,
 619}
 620
 621impl PartialEq for FsEntryDirectory {
 622    fn eq(&self, other: &Self) -> bool {
 623        self.worktree_id == other.worktree_id && self.entry.id == other.entry.id
 624    }
 625}
 626
 627impl Hash for FsEntryDirectory {
 628    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
 629        (self.worktree_id, self.entry.id).hash(state);
 630    }
 631}
 632
 633#[derive(Debug, Clone, Eq)]
 634struct FsEntryExternalFile {
 635    buffer_id: BufferId,
 636    excerpts: Vec<ExcerptId>,
 637}
 638
 639impl PartialEq for FsEntryExternalFile {
 640    fn eq(&self, other: &Self) -> bool {
 641        self.buffer_id == other.buffer_id
 642    }
 643}
 644
 645impl Hash for FsEntryExternalFile {
 646    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
 647        self.buffer_id.hash(state);
 648    }
 649}
 650
 651#[derive(Clone, Debug, Eq, PartialEq)]
 652enum FsEntry {
 653    ExternalFile(FsEntryExternalFile),
 654    Directory(FsEntryDirectory),
 655    File(FsEntryFile),
 656}
 657
 658struct ActiveItem {
 659    item_handle: Box<dyn WeakItemHandle>,
 660    active_editor: WeakEntity<Editor>,
 661    _buffer_search_subscription: Subscription,
 662    _editor_subscription: Subscription,
 663}
 664
 665#[derive(Debug)]
 666pub enum Event {
 667    Focus,
 668}
 669
 670#[derive(Serialize, Deserialize)]
 671struct SerializedOutlinePanel {
 672    width: Option<Pixels>,
 673    active: Option<bool>,
 674}
 675
 676pub fn init_settings(cx: &mut App) {
 677    OutlinePanelSettings::register(cx);
 678}
 679
 680pub fn init(cx: &mut App) {
 681    init_settings(cx);
 682
 683    cx.observe_new(|workspace: &mut Workspace, _, _| {
 684        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
 685            workspace.toggle_panel_focus::<OutlinePanel>(window, cx);
 686        });
 687    })
 688    .detach();
 689}
 690
 691impl OutlinePanel {
 692    pub async fn load(
 693        workspace: WeakEntity<Workspace>,
 694        mut cx: AsyncWindowContext,
 695    ) -> anyhow::Result<Entity<Self>> {
 696        let serialized_panel = match workspace
 697            .read_with(&cx, |workspace, _| {
 698                OutlinePanel::serialization_key(workspace)
 699            })
 700            .ok()
 701            .flatten()
 702        {
 703            Some(serialization_key) => cx
 704                .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) })
 705                .await
 706                .context("loading outline panel")
 707                .log_err()
 708                .flatten()
 709                .map(|panel| serde_json::from_str::<SerializedOutlinePanel>(&panel))
 710                .transpose()
 711                .log_err()
 712                .flatten(),
 713            None => None,
 714        };
 715
 716        workspace.update_in(&mut cx, |workspace, window, cx| {
 717            let panel = Self::new(workspace, window, cx);
 718            if let Some(serialized_panel) = serialized_panel {
 719                panel.update(cx, |panel, cx| {
 720                    panel.width = serialized_panel.width.map(|px| px.round());
 721                    panel.active = serialized_panel.active.unwrap_or(false);
 722                    cx.notify();
 723                });
 724            }
 725            panel
 726        })
 727    }
 728
 729    fn new(
 730        workspace: &mut Workspace,
 731        window: &mut Window,
 732        cx: &mut Context<Workspace>,
 733    ) -> Entity<Self> {
 734        let project = workspace.project().clone();
 735        let workspace_handle = cx.entity().downgrade();
 736
 737        cx.new(|cx| {
 738            let filter_editor = cx.new(|cx| {
 739                let mut editor = Editor::single_line(window, cx);
 740                editor.set_placeholder_text("Filter...", cx);
 741                editor
 742            });
 743            let filter_update_subscription = cx.subscribe_in(
 744                &filter_editor,
 745                window,
 746                |outline_panel: &mut Self, _, event, window, cx| {
 747                    if let editor::EditorEvent::BufferEdited = event {
 748                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
 749                    }
 750                },
 751            );
 752
 753            let focus_handle = cx.focus_handle();
 754            let focus_subscription = cx.on_focus(&focus_handle, window, Self::focus_in);
 755            let focus_out_subscription =
 756                cx.on_focus_out(&focus_handle, window, |outline_panel, _, window, cx| {
 757                    outline_panel.hide_scrollbar(window, cx);
 758                });
 759            let workspace_subscription = cx.subscribe_in(
 760                &workspace
 761                    .weak_handle()
 762                    .upgrade()
 763                    .expect("have a &mut Workspace"),
 764                window,
 765                move |outline_panel, workspace, event, window, cx| {
 766                    if let workspace::Event::ActiveItemChanged = event {
 767                        if let Some((new_active_item, new_active_editor)) =
 768                            workspace_active_editor(workspace.read(cx), cx)
 769                        {
 770                            if outline_panel.should_replace_active_item(new_active_item.as_ref()) {
 771                                outline_panel.replace_active_editor(
 772                                    new_active_item,
 773                                    new_active_editor,
 774                                    window,
 775                                    cx,
 776                                );
 777                            }
 778                        } else {
 779                            outline_panel.clear_previous(window, cx);
 780                            cx.notify();
 781                        }
 782                    }
 783                },
 784            );
 785
 786            let icons_subscription = cx.observe_global::<FileIcons>(|_, cx| {
 787                cx.notify();
 788            });
 789
 790            let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx);
 791            let mut current_theme = ThemeSettings::get_global(cx).clone();
 792            let settings_subscription =
 793                cx.observe_global_in::<SettingsStore>(window, move |outline_panel, window, cx| {
 794                    let new_settings = OutlinePanelSettings::get_global(cx);
 795                    let new_theme = ThemeSettings::get_global(cx);
 796                    if &current_theme != new_theme {
 797                        outline_panel_settings = *new_settings;
 798                        current_theme = new_theme.clone();
 799                        for excerpts in outline_panel.excerpts.values_mut() {
 800                            for excerpt in excerpts.values_mut() {
 801                                excerpt.invalidate_outlines();
 802                            }
 803                        }
 804                        let update_cached_items = outline_panel.update_non_fs_items(window, cx);
 805                        if update_cached_items {
 806                            outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
 807                        }
 808                    } else if &outline_panel_settings != new_settings {
 809                        let old_expansion_depth = outline_panel_settings.expand_outlines_with_depth;
 810                        outline_panel_settings = *new_settings;
 811
 812                        if old_expansion_depth != new_settings.expand_outlines_with_depth {
 813                            let old_collapsed_entries = outline_panel.collapsed_entries.clone();
 814                            outline_panel
 815                                .collapsed_entries
 816                                .retain(|entry| !matches!(entry, CollapsedEntry::Outline(..)));
 817
 818                            let new_depth = new_settings.expand_outlines_with_depth;
 819
 820                            for (buffer_id, excerpts) in &outline_panel.excerpts {
 821                                for (excerpt_id, excerpt) in excerpts {
 822                                    if let ExcerptOutlines::Outlines(outlines) = &excerpt.outlines {
 823                                        for outline in outlines {
 824                                            if outline_panel
 825                                                .outline_children_cache
 826                                                .get(buffer_id)
 827                                                .and_then(|children_map| {
 828                                                    let key =
 829                                                        (outline.range.clone(), outline.depth);
 830                                                    children_map.get(&key)
 831                                                })
 832                                                .copied()
 833                                                .unwrap_or(false)
 834                                                && (new_depth == 0 || outline.depth >= new_depth)
 835                                            {
 836                                                outline_panel.collapsed_entries.insert(
 837                                                    CollapsedEntry::Outline(
 838                                                        *buffer_id,
 839                                                        *excerpt_id,
 840                                                        outline.range.clone(),
 841                                                    ),
 842                                                );
 843                                            }
 844                                        }
 845                                    }
 846                                }
 847                            }
 848
 849                            if old_collapsed_entries != outline_panel.collapsed_entries {
 850                                outline_panel.update_cached_entries(
 851                                    Some(UPDATE_DEBOUNCE),
 852                                    window,
 853                                    cx,
 854                                );
 855                            }
 856                        } else {
 857                            cx.notify();
 858                        }
 859                    }
 860                });
 861
 862            let scroll_handle = UniformListScrollHandle::new();
 863
 864            let mut outline_panel = Self {
 865                mode: ItemsDisplayMode::Outline,
 866                active: false,
 867                pinned: false,
 868                workspace: workspace_handle,
 869                project,
 870                fs: workspace.app_state().fs.clone(),
 871                show_scrollbar: !Self::should_autohide_scrollbar(cx),
 872                hide_scrollbar_task: None,
 873                vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
 874                    .parent_entity(&cx.entity()),
 875                horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
 876                    .parent_entity(&cx.entity()),
 877                max_width_item_index: None,
 878                scroll_handle,
 879                focus_handle,
 880                filter_editor,
 881                fs_entries: Vec::new(),
 882                fs_entries_depth: HashMap::default(),
 883                fs_children_count: HashMap::default(),
 884                collapsed_entries: HashSet::default(),
 885                unfolded_dirs: HashMap::default(),
 886                selected_entry: SelectedEntry::None,
 887                context_menu: None,
 888                width: None,
 889                active_item: None,
 890                pending_serialization: Task::ready(None),
 891                updating_fs_entries: false,
 892                updating_cached_entries: false,
 893                new_entries_for_fs_update: HashSet::default(),
 894                preserve_selection_on_buffer_fold_toggles: HashSet::default(),
 895                pending_default_expansion_depth: None,
 896                fs_entries_update_task: Task::ready(()),
 897                cached_entries_update_task: Task::ready(()),
 898                reveal_selection_task: Task::ready(Ok(())),
 899                outline_fetch_tasks: HashMap::default(),
 900                excerpts: HashMap::default(),
 901                cached_entries: Vec::new(),
 902                _subscriptions: vec![
 903                    settings_subscription,
 904                    icons_subscription,
 905                    focus_subscription,
 906                    focus_out_subscription,
 907                    workspace_subscription,
 908                    filter_update_subscription,
 909                ],
 910                outline_children_cache: HashMap::default(),
 911            };
 912            if let Some((item, editor)) = workspace_active_editor(workspace, cx) {
 913                outline_panel.replace_active_editor(item, editor, window, cx);
 914            }
 915            outline_panel
 916        })
 917    }
 918
 919    fn serialization_key(workspace: &Workspace) -> Option<String> {
 920        workspace
 921            .database_id()
 922            .map(|id| i64::from(id).to_string())
 923            .or(workspace.session_id())
 924            .map(|id| format!("{}-{:?}", OUTLINE_PANEL_KEY, id))
 925    }
 926
 927    fn serialize(&mut self, cx: &mut Context<Self>) {
 928        let Some(serialization_key) = self
 929            .workspace
 930            .read_with(cx, |workspace, _| {
 931                OutlinePanel::serialization_key(workspace)
 932            })
 933            .ok()
 934            .flatten()
 935        else {
 936            return;
 937        };
 938        let width = self.width;
 939        let active = Some(self.active);
 940        self.pending_serialization = cx.background_spawn(
 941            async move {
 942                KEY_VALUE_STORE
 943                    .write_kvp(
 944                        serialization_key,
 945                        serde_json::to_string(&SerializedOutlinePanel { width, active })?,
 946                    )
 947                    .await?;
 948                anyhow::Ok(())
 949            }
 950            .log_err(),
 951        );
 952    }
 953
 954    fn dispatch_context(&self, window: &mut Window, cx: &mut Context<Self>) -> KeyContext {
 955        let mut dispatch_context = KeyContext::new_with_defaults();
 956        dispatch_context.add("OutlinePanel");
 957        dispatch_context.add("menu");
 958        let identifier = if self.filter_editor.focus_handle(cx).is_focused(window) {
 959            "editing"
 960        } else {
 961            "not_editing"
 962        };
 963        dispatch_context.add(identifier);
 964        dispatch_context
 965    }
 966
 967    fn unfold_directory(
 968        &mut self,
 969        _: &UnfoldDirectory,
 970        window: &mut Window,
 971        cx: &mut Context<Self>,
 972    ) {
 973        if let Some(PanelEntry::FoldedDirs(FoldedDirsEntry {
 974            worktree_id,
 975            entries,
 976            ..
 977        })) = self.selected_entry().cloned()
 978        {
 979            self.unfolded_dirs
 980                .entry(worktree_id)
 981                .or_default()
 982                .extend(entries.iter().map(|entry| entry.id));
 983            self.update_cached_entries(None, window, cx);
 984        }
 985    }
 986
 987    fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context<Self>) {
 988        let (worktree_id, entry) = match self.selected_entry().cloned() {
 989            Some(PanelEntry::Fs(FsEntry::Directory(directory))) => {
 990                (directory.worktree_id, Some(directory.entry))
 991            }
 992            Some(PanelEntry::FoldedDirs(folded_dirs)) => {
 993                (folded_dirs.worktree_id, folded_dirs.entries.last().cloned())
 994            }
 995            _ => return,
 996        };
 997        let Some(entry) = entry else {
 998            return;
 999        };
1000        let unfolded_dirs = self.unfolded_dirs.get_mut(&worktree_id);
1001        let worktree = self
1002            .project
1003            .read(cx)
1004            .worktree_for_id(worktree_id, cx)
1005            .map(|w| w.read(cx).snapshot());
1006        let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
1007            return;
1008        };
1009
1010        unfolded_dirs.remove(&entry.id);
1011        self.update_cached_entries(None, window, cx);
1012    }
1013
1014    fn open_selected_entry(
1015        &mut self,
1016        _: &OpenSelectedEntry,
1017        window: &mut Window,
1018        cx: &mut Context<Self>,
1019    ) {
1020        if self.filter_editor.focus_handle(cx).is_focused(window) {
1021            cx.propagate()
1022        } else if let Some(selected_entry) = self.selected_entry().cloned() {
1023            self.toggle_expanded(&selected_entry, window, cx);
1024            self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
1025        }
1026    }
1027
1028    fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
1029        if self.filter_editor.focus_handle(cx).is_focused(window) {
1030            self.focus_handle.focus(window);
1031        } else {
1032            self.filter_editor.focus_handle(cx).focus(window);
1033        }
1034
1035        if self.context_menu.is_some() {
1036            self.context_menu.take();
1037            cx.notify();
1038        }
1039    }
1040
1041    fn open_excerpts(
1042        &mut self,
1043        action: &editor::actions::OpenExcerpts,
1044        window: &mut Window,
1045        cx: &mut Context<Self>,
1046    ) {
1047        if self.filter_editor.focus_handle(cx).is_focused(window) {
1048            cx.propagate()
1049        } else if let Some((active_editor, selected_entry)) =
1050            self.active_editor().zip(self.selected_entry().cloned())
1051        {
1052            self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
1053            active_editor.update(cx, |editor, cx| editor.open_excerpts(action, window, cx));
1054        }
1055    }
1056
1057    fn open_excerpts_split(
1058        &mut self,
1059        action: &editor::actions::OpenExcerptsSplit,
1060        window: &mut Window,
1061        cx: &mut Context<Self>,
1062    ) {
1063        if self.filter_editor.focus_handle(cx).is_focused(window) {
1064            cx.propagate()
1065        } else if let Some((active_editor, selected_entry)) =
1066            self.active_editor().zip(self.selected_entry().cloned())
1067        {
1068            self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
1069            active_editor.update(cx, |editor, cx| {
1070                editor.open_excerpts_in_split(action, window, cx)
1071            });
1072        }
1073    }
1074
1075    fn scroll_editor_to_entry(
1076        &mut self,
1077        entry: &PanelEntry,
1078        prefer_selection_change: bool,
1079        prefer_focus_change: bool,
1080        window: &mut Window,
1081        cx: &mut Context<OutlinePanel>,
1082    ) {
1083        let Some(active_editor) = self.active_editor() else {
1084            return;
1085        };
1086        let active_multi_buffer = active_editor.read(cx).buffer().clone();
1087        let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
1088        let mut change_selection = prefer_selection_change;
1089        let mut change_focus = prefer_focus_change;
1090        let mut scroll_to_buffer = None;
1091        let scroll_target = match entry {
1092            PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => {
1093                change_focus = false;
1094                None
1095            }
1096            PanelEntry::Fs(FsEntry::ExternalFile(file)) => {
1097                change_selection = false;
1098                scroll_to_buffer = Some(file.buffer_id);
1099                multi_buffer_snapshot.excerpts().find_map(
1100                    |(excerpt_id, buffer_snapshot, excerpt_range)| {
1101                        if buffer_snapshot.remote_id() == file.buffer_id {
1102                            multi_buffer_snapshot
1103                                .anchor_in_excerpt(excerpt_id, excerpt_range.context.start)
1104                        } else {
1105                            None
1106                        }
1107                    },
1108                )
1109            }
1110
1111            PanelEntry::Fs(FsEntry::File(file)) => {
1112                change_selection = false;
1113                scroll_to_buffer = Some(file.buffer_id);
1114                self.project
1115                    .update(cx, |project, cx| {
1116                        project
1117                            .path_for_entry(file.entry.id, cx)
1118                            .and_then(|path| project.get_open_buffer(&path, cx))
1119                    })
1120                    .map(|buffer| {
1121                        active_multi_buffer
1122                            .read(cx)
1123                            .excerpts_for_buffer(buffer.read(cx).remote_id(), cx)
1124                    })
1125                    .and_then(|excerpts| {
1126                        let (excerpt_id, excerpt_range) = excerpts.first()?;
1127                        multi_buffer_snapshot
1128                            .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
1129                    })
1130            }
1131            PanelEntry::Outline(OutlineEntry::Outline(outline)) => multi_buffer_snapshot
1132                .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.start)
1133                .or_else(|| {
1134                    multi_buffer_snapshot
1135                        .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.end)
1136                }),
1137            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1138                change_selection = false;
1139                change_focus = false;
1140                multi_buffer_snapshot.anchor_in_excerpt(excerpt.id, excerpt.range.context.start)
1141            }
1142            PanelEntry::Search(search_entry) => Some(search_entry.match_range.start),
1143        };
1144
1145        if let Some(anchor) = scroll_target {
1146            let activate = self
1147                .workspace
1148                .update(cx, |workspace, cx| match self.active_item() {
1149                    Some(active_item) => workspace.activate_item(
1150                        active_item.as_ref(),
1151                        true,
1152                        change_focus,
1153                        window,
1154                        cx,
1155                    ),
1156                    None => workspace.activate_item(&active_editor, true, change_focus, window, cx),
1157                });
1158
1159            if activate.is_ok() {
1160                self.select_entry(entry.clone(), true, window, cx);
1161                if change_selection {
1162                    active_editor.update(cx, |editor, cx| {
1163                        editor.change_selections(
1164                            SelectionEffects::scroll(Autoscroll::center()),
1165                            window,
1166                            cx,
1167                            |s| s.select_ranges(Some(anchor..anchor)),
1168                        );
1169                    });
1170                } else {
1171                    let mut offset = Point::default();
1172                    if let Some(buffer_id) = scroll_to_buffer
1173                        && multi_buffer_snapshot.as_singleton().is_none()
1174                        && !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
1175                    {
1176                        offset.y = -(active_editor.read(cx).file_header_size() as f32);
1177                    }
1178
1179                    active_editor.update(cx, |editor, cx| {
1180                        editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, window, cx);
1181                    });
1182                }
1183
1184                if change_focus {
1185                    active_editor.focus_handle(cx).focus(window);
1186                } else {
1187                    self.focus_handle.focus(window);
1188                }
1189            }
1190        }
1191    }
1192
1193    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
1194        if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
1195            self.cached_entries
1196                .iter()
1197                .map(|cached_entry| &cached_entry.entry)
1198                .skip_while(|entry| entry != &selected_entry)
1199                .nth(1)
1200                .cloned()
1201        }) {
1202            self.select_entry(entry_to_select, true, window, cx);
1203        } else {
1204            self.select_first(&SelectFirst {}, window, cx)
1205        }
1206        if let Some(selected_entry) = self.selected_entry().cloned() {
1207            self.scroll_editor_to_entry(&selected_entry, true, false, window, cx);
1208        }
1209    }
1210
1211    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
1212        if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
1213            self.cached_entries
1214                .iter()
1215                .rev()
1216                .map(|cached_entry| &cached_entry.entry)
1217                .skip_while(|entry| entry != &selected_entry)
1218                .nth(1)
1219                .cloned()
1220        }) {
1221            self.select_entry(entry_to_select, true, window, cx);
1222        } else {
1223            self.select_last(&SelectLast, window, cx)
1224        }
1225        if let Some(selected_entry) = self.selected_entry().cloned() {
1226            self.scroll_editor_to_entry(&selected_entry, true, false, window, cx);
1227        }
1228    }
1229
1230    fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context<Self>) {
1231        if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| {
1232            let mut previous_entries = self
1233                .cached_entries
1234                .iter()
1235                .rev()
1236                .map(|cached_entry| &cached_entry.entry)
1237                .skip_while(|entry| entry != &selected_entry)
1238                .skip(1);
1239            match &selected_entry {
1240                PanelEntry::Fs(fs_entry) => match fs_entry {
1241                    FsEntry::ExternalFile(..) => None,
1242                    FsEntry::File(FsEntryFile {
1243                        worktree_id, entry, ..
1244                    })
1245                    | FsEntry::Directory(FsEntryDirectory {
1246                        worktree_id, entry, ..
1247                    }) => entry.path.parent().and_then(|parent_path| {
1248                        previous_entries.find(|entry| match entry {
1249                            PanelEntry::Fs(FsEntry::Directory(directory)) => {
1250                                directory.worktree_id == *worktree_id
1251                                    && directory.entry.path.as_ref() == parent_path
1252                            }
1253                            PanelEntry::FoldedDirs(FoldedDirsEntry {
1254                                worktree_id: dirs_worktree_id,
1255                                entries: dirs,
1256                                ..
1257                            }) => {
1258                                dirs_worktree_id == worktree_id
1259                                    && dirs
1260                                        .last()
1261                                        .is_some_and(|dir| dir.path.as_ref() == parent_path)
1262                            }
1263                            _ => false,
1264                        })
1265                    }),
1266                },
1267                PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
1268                    .entries
1269                    .first()
1270                    .and_then(|entry| entry.path.parent())
1271                    .and_then(|parent_path| {
1272                        previous_entries.find(|entry| {
1273                            if let PanelEntry::Fs(FsEntry::Directory(directory)) = entry {
1274                                directory.worktree_id == folded_dirs.worktree_id
1275                                    && directory.entry.path.as_ref() == parent_path
1276                            } else {
1277                                false
1278                            }
1279                        })
1280                    }),
1281                PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1282                    previous_entries.find(|entry| match entry {
1283                        PanelEntry::Fs(FsEntry::File(file)) => {
1284                            file.buffer_id == excerpt.buffer_id
1285                                && file.excerpts.contains(&excerpt.id)
1286                        }
1287                        PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1288                            external_file.buffer_id == excerpt.buffer_id
1289                                && external_file.excerpts.contains(&excerpt.id)
1290                        }
1291                        _ => false,
1292                    })
1293                }
1294                PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1295                    previous_entries.find(|entry| {
1296                        if let PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) = entry {
1297                            outline.buffer_id == excerpt.buffer_id
1298                                && outline.excerpt_id == excerpt.id
1299                        } else {
1300                            false
1301                        }
1302                    })
1303                }
1304                PanelEntry::Search(_) => {
1305                    previous_entries.find(|entry| !matches!(entry, PanelEntry::Search(_)))
1306                }
1307            }
1308        }) {
1309            self.select_entry(entry_to_select.clone(), true, window, cx);
1310        } else {
1311            self.select_first(&SelectFirst {}, window, cx);
1312        }
1313    }
1314
1315    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
1316        if let Some(first_entry) = self.cached_entries.first() {
1317            self.select_entry(first_entry.entry.clone(), true, window, cx);
1318        }
1319    }
1320
1321    fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
1322        if let Some(new_selection) = self
1323            .cached_entries
1324            .iter()
1325            .rev()
1326            .map(|cached_entry| &cached_entry.entry)
1327            .next()
1328        {
1329            self.select_entry(new_selection.clone(), true, window, cx);
1330        }
1331    }
1332
1333    fn autoscroll(&mut self, cx: &mut Context<Self>) {
1334        if let Some(selected_entry) = self.selected_entry() {
1335            let index = self
1336                .cached_entries
1337                .iter()
1338                .position(|cached_entry| &cached_entry.entry == selected_entry);
1339            if let Some(index) = index {
1340                self.scroll_handle
1341                    .scroll_to_item(index, ScrollStrategy::Center);
1342                cx.notify();
1343            }
1344        }
1345    }
1346
1347    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1348        if !self.focus_handle.contains_focused(window, cx) {
1349            cx.emit(Event::Focus);
1350        }
1351    }
1352
1353    fn deploy_context_menu(
1354        &mut self,
1355        position: Point<Pixels>,
1356        entry: PanelEntry,
1357        window: &mut Window,
1358        cx: &mut Context<Self>,
1359    ) {
1360        self.select_entry(entry.clone(), true, window, cx);
1361        let is_root = match &entry {
1362            PanelEntry::Fs(FsEntry::File(FsEntryFile {
1363                worktree_id, entry, ..
1364            }))
1365            | PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1366                worktree_id, entry, ..
1367            })) => self
1368                .project
1369                .read(cx)
1370                .worktree_for_id(*worktree_id, cx)
1371                .map(|worktree| {
1372                    worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
1373                })
1374                .unwrap_or(false),
1375            PanelEntry::FoldedDirs(FoldedDirsEntry {
1376                worktree_id,
1377                entries,
1378                ..
1379            }) => entries
1380                .first()
1381                .and_then(|entry| {
1382                    self.project
1383                        .read(cx)
1384                        .worktree_for_id(*worktree_id, cx)
1385                        .map(|worktree| {
1386                            worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
1387                        })
1388                })
1389                .unwrap_or(false),
1390            PanelEntry::Fs(FsEntry::ExternalFile(..)) => false,
1391            PanelEntry::Outline(..) => {
1392                cx.notify();
1393                return;
1394            }
1395            PanelEntry::Search(_) => {
1396                cx.notify();
1397                return;
1398            }
1399        };
1400        let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
1401        let is_foldable = auto_fold_dirs && !is_root && self.is_foldable(&entry);
1402        let is_unfoldable = auto_fold_dirs && !is_root && self.is_unfoldable(&entry);
1403
1404        let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
1405            menu.context(self.focus_handle.clone())
1406                .when(cfg!(target_os = "macos"), |menu| {
1407                    menu.action("Reveal in Finder", Box::new(RevealInFileManager))
1408                })
1409                .when(cfg!(not(target_os = "macos")), |menu| {
1410                    menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
1411                })
1412                .action("Open in Terminal", Box::new(OpenInTerminal))
1413                .when(is_unfoldable, |menu| {
1414                    menu.action("Unfold Directory", Box::new(UnfoldDirectory))
1415                })
1416                .when(is_foldable, |menu| {
1417                    menu.action("Fold Directory", Box::new(FoldDirectory))
1418                })
1419                .separator()
1420                .action("Copy Path", Box::new(zed_actions::workspace::CopyPath))
1421                .action(
1422                    "Copy Relative Path",
1423                    Box::new(zed_actions::workspace::CopyRelativePath),
1424                )
1425        });
1426        window.focus(&context_menu.focus_handle(cx));
1427        let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| {
1428            outline_panel.context_menu.take();
1429            cx.notify();
1430        });
1431        self.context_menu = Some((context_menu, position, subscription));
1432        cx.notify();
1433    }
1434
1435    fn is_unfoldable(&self, entry: &PanelEntry) -> bool {
1436        matches!(entry, PanelEntry::FoldedDirs(..))
1437    }
1438
1439    fn is_foldable(&self, entry: &PanelEntry) -> bool {
1440        let (directory_worktree, directory_entry) = match entry {
1441            PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1442                worktree_id,
1443                entry: directory_entry,
1444                ..
1445            })) => (*worktree_id, Some(directory_entry)),
1446            _ => return false,
1447        };
1448        let Some(directory_entry) = directory_entry else {
1449            return false;
1450        };
1451
1452        if self
1453            .unfolded_dirs
1454            .get(&directory_worktree)
1455            .is_none_or(|unfolded_dirs| !unfolded_dirs.contains(&directory_entry.id))
1456        {
1457            return false;
1458        }
1459
1460        let children = self
1461            .fs_children_count
1462            .get(&directory_worktree)
1463            .and_then(|entries| entries.get(&directory_entry.path))
1464            .copied()
1465            .unwrap_or_default();
1466
1467        children.may_be_fold_part() && children.dirs > 0
1468    }
1469
1470    fn expand_selected_entry(
1471        &mut self,
1472        _: &ExpandSelectedEntry,
1473        window: &mut Window,
1474        cx: &mut Context<Self>,
1475    ) {
1476        let Some(active_editor) = self.active_editor() else {
1477            return;
1478        };
1479        let Some(selected_entry) = self.selected_entry().cloned() else {
1480            return;
1481        };
1482        let mut buffers_to_unfold = HashSet::default();
1483        let entry_to_expand = match &selected_entry {
1484            PanelEntry::FoldedDirs(FoldedDirsEntry {
1485                entries: dir_entries,
1486                worktree_id,
1487                ..
1488            }) => dir_entries.last().map(|entry| {
1489                buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry));
1490                CollapsedEntry::Dir(*worktree_id, entry.id)
1491            }),
1492            PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1493                worktree_id, entry, ..
1494            })) => {
1495                buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry));
1496                Some(CollapsedEntry::Dir(*worktree_id, entry.id))
1497            }
1498            PanelEntry::Fs(FsEntry::File(FsEntryFile {
1499                worktree_id,
1500                buffer_id,
1501                ..
1502            })) => {
1503                buffers_to_unfold.insert(*buffer_id);
1504                Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1505            }
1506            PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1507                buffers_to_unfold.insert(external_file.buffer_id);
1508                Some(CollapsedEntry::ExternalFile(external_file.buffer_id))
1509            }
1510            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1511                Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id))
1512            }
1513            PanelEntry::Outline(OutlineEntry::Outline(outline)) => Some(CollapsedEntry::Outline(
1514                outline.buffer_id,
1515                outline.excerpt_id,
1516                outline.outline.range.clone(),
1517            )),
1518            PanelEntry::Search(_) => return,
1519        };
1520        let Some(collapsed_entry) = entry_to_expand else {
1521            return;
1522        };
1523        let expanded = self.collapsed_entries.remove(&collapsed_entry);
1524        if expanded {
1525            if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
1526                let task = self.project.update(cx, |project, cx| {
1527                    project.expand_entry(worktree_id, dir_entry_id, cx)
1528                });
1529                if let Some(task) = task {
1530                    task.detach_and_log_err(cx);
1531                }
1532            };
1533
1534            active_editor.update(cx, |editor, cx| {
1535                buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx));
1536            });
1537            self.select_entry(selected_entry, true, window, cx);
1538            if buffers_to_unfold.is_empty() {
1539                self.update_cached_entries(None, window, cx);
1540            } else {
1541                self.toggle_buffers_fold(buffers_to_unfold, false, window, cx)
1542                    .detach();
1543            }
1544        } else {
1545            self.select_next(&SelectNext, window, cx)
1546        }
1547    }
1548
1549    fn collapse_selected_entry(
1550        &mut self,
1551        _: &CollapseSelectedEntry,
1552        window: &mut Window,
1553        cx: &mut Context<Self>,
1554    ) {
1555        let Some(active_editor) = self.active_editor() else {
1556            return;
1557        };
1558        let Some(selected_entry) = self.selected_entry().cloned() else {
1559            return;
1560        };
1561
1562        let mut buffers_to_fold = HashSet::default();
1563        let collapsed = match &selected_entry {
1564            PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1565                worktree_id, entry, ..
1566            })) => {
1567                if self
1568                    .collapsed_entries
1569                    .insert(CollapsedEntry::Dir(*worktree_id, entry.id))
1570                {
1571                    buffers_to_fold.extend(self.buffers_inside_directory(*worktree_id, entry));
1572                    true
1573                } else {
1574                    false
1575                }
1576            }
1577            PanelEntry::Fs(FsEntry::File(FsEntryFile {
1578                worktree_id,
1579                buffer_id,
1580                ..
1581            })) => {
1582                if self
1583                    .collapsed_entries
1584                    .insert(CollapsedEntry::File(*worktree_id, *buffer_id))
1585                {
1586                    buffers_to_fold.insert(*buffer_id);
1587                    true
1588                } else {
1589                    false
1590                }
1591            }
1592            PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1593                if self
1594                    .collapsed_entries
1595                    .insert(CollapsedEntry::ExternalFile(external_file.buffer_id))
1596                {
1597                    buffers_to_fold.insert(external_file.buffer_id);
1598                    true
1599                } else {
1600                    false
1601                }
1602            }
1603            PanelEntry::FoldedDirs(folded_dirs) => {
1604                let mut folded = false;
1605                if let Some(dir_entry) = folded_dirs.entries.last()
1606                    && self
1607                        .collapsed_entries
1608                        .insert(CollapsedEntry::Dir(folded_dirs.worktree_id, dir_entry.id))
1609                {
1610                    folded = true;
1611                    buffers_to_fold
1612                        .extend(self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry));
1613                }
1614                folded
1615            }
1616            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
1617                .collapsed_entries
1618                .insert(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)),
1619            PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1620                self.collapsed_entries.insert(CollapsedEntry::Outline(
1621                    outline.buffer_id,
1622                    outline.excerpt_id,
1623                    outline.outline.range.clone(),
1624                ))
1625            }
1626            PanelEntry::Search(_) => false,
1627        };
1628
1629        if collapsed {
1630            active_editor.update(cx, |editor, cx| {
1631                buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx));
1632            });
1633            self.select_entry(selected_entry, true, window, cx);
1634            if buffers_to_fold.is_empty() {
1635                self.update_cached_entries(None, window, cx);
1636            } else {
1637                self.toggle_buffers_fold(buffers_to_fold, true, window, cx)
1638                    .detach();
1639            }
1640        } else {
1641            self.select_parent(&SelectParent, window, cx);
1642        }
1643    }
1644
1645    pub fn expand_all_entries(
1646        &mut self,
1647        _: &ExpandAllEntries,
1648        window: &mut Window,
1649        cx: &mut Context<Self>,
1650    ) {
1651        let Some(active_editor) = self.active_editor() else {
1652            return;
1653        };
1654        let mut buffers_to_unfold = HashSet::default();
1655        let expanded_entries =
1656            self.fs_entries
1657                .iter()
1658                .fold(HashSet::default(), |mut entries, fs_entry| {
1659                    match fs_entry {
1660                        FsEntry::ExternalFile(external_file) => {
1661                            buffers_to_unfold.insert(external_file.buffer_id);
1662                            entries.insert(CollapsedEntry::ExternalFile(external_file.buffer_id));
1663                            entries.extend(
1664                                self.excerpts
1665                                    .get(&external_file.buffer_id)
1666                                    .into_iter()
1667                                    .flat_map(|excerpts| {
1668                                        excerpts.keys().map(|excerpt_id| {
1669                                            CollapsedEntry::Excerpt(
1670                                                external_file.buffer_id,
1671                                                *excerpt_id,
1672                                            )
1673                                        })
1674                                    }),
1675                            );
1676                        }
1677                        FsEntry::Directory(directory) => {
1678                            entries.insert(CollapsedEntry::Dir(
1679                                directory.worktree_id,
1680                                directory.entry.id,
1681                            ));
1682                        }
1683                        FsEntry::File(file) => {
1684                            buffers_to_unfold.insert(file.buffer_id);
1685                            entries.insert(CollapsedEntry::File(file.worktree_id, file.buffer_id));
1686                            entries.extend(
1687                                self.excerpts.get(&file.buffer_id).into_iter().flat_map(
1688                                    |excerpts| {
1689                                        excerpts.keys().map(|excerpt_id| {
1690                                            CollapsedEntry::Excerpt(file.buffer_id, *excerpt_id)
1691                                        })
1692                                    },
1693                                ),
1694                            );
1695                        }
1696                    };
1697                    entries
1698                });
1699        self.collapsed_entries
1700            .retain(|entry| !expanded_entries.contains(entry));
1701        active_editor.update(cx, |editor, cx| {
1702            buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx));
1703        });
1704        if buffers_to_unfold.is_empty() {
1705            self.update_cached_entries(None, window, cx);
1706        } else {
1707            self.toggle_buffers_fold(buffers_to_unfold, false, window, cx)
1708                .detach();
1709        }
1710    }
1711
1712    pub fn collapse_all_entries(
1713        &mut self,
1714        _: &CollapseAllEntries,
1715        window: &mut Window,
1716        cx: &mut Context<Self>,
1717    ) {
1718        let Some(active_editor) = self.active_editor() else {
1719            return;
1720        };
1721        let mut buffers_to_fold = HashSet::default();
1722        let new_entries = self
1723            .cached_entries
1724            .iter()
1725            .flat_map(|cached_entry| match &cached_entry.entry {
1726                PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1727                    worktree_id, entry, ..
1728                })) => Some(CollapsedEntry::Dir(*worktree_id, entry.id)),
1729                PanelEntry::Fs(FsEntry::File(FsEntryFile {
1730                    worktree_id,
1731                    buffer_id,
1732                    ..
1733                })) => {
1734                    buffers_to_fold.insert(*buffer_id);
1735                    Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1736                }
1737                PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1738                    buffers_to_fold.insert(external_file.buffer_id);
1739                    Some(CollapsedEntry::ExternalFile(external_file.buffer_id))
1740                }
1741                PanelEntry::FoldedDirs(FoldedDirsEntry {
1742                    worktree_id,
1743                    entries,
1744                    ..
1745                }) => Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)),
1746                PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1747                    Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id))
1748                }
1749                PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1750            })
1751            .collect::<Vec<_>>();
1752        self.collapsed_entries.extend(new_entries);
1753
1754        active_editor.update(cx, |editor, cx| {
1755            buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx));
1756        });
1757        if buffers_to_fold.is_empty() {
1758            self.update_cached_entries(None, window, cx);
1759        } else {
1760            self.toggle_buffers_fold(buffers_to_fold, true, window, cx)
1761                .detach();
1762        }
1763    }
1764
1765    fn toggle_expanded(&mut self, entry: &PanelEntry, window: &mut Window, cx: &mut Context<Self>) {
1766        let Some(active_editor) = self.active_editor() else {
1767            return;
1768        };
1769        let mut fold = false;
1770        let mut buffers_to_toggle = HashSet::default();
1771        match entry {
1772            PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
1773                worktree_id,
1774                entry: dir_entry,
1775                ..
1776            })) => {
1777                let entry_id = dir_entry.id;
1778                let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1779                buffers_to_toggle.extend(self.buffers_inside_directory(*worktree_id, dir_entry));
1780                if self.collapsed_entries.remove(&collapsed_entry) {
1781                    self.project
1782                        .update(cx, |project, cx| {
1783                            project.expand_entry(*worktree_id, entry_id, cx)
1784                        })
1785                        .unwrap_or_else(|| Task::ready(Ok(())))
1786                        .detach_and_log_err(cx);
1787                } else {
1788                    self.collapsed_entries.insert(collapsed_entry);
1789                    fold = true;
1790                }
1791            }
1792            PanelEntry::Fs(FsEntry::File(FsEntryFile {
1793                worktree_id,
1794                buffer_id,
1795                ..
1796            })) => {
1797                let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
1798                buffers_to_toggle.insert(*buffer_id);
1799                if !self.collapsed_entries.remove(&collapsed_entry) {
1800                    self.collapsed_entries.insert(collapsed_entry);
1801                    fold = true;
1802                }
1803            }
1804            PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => {
1805                let collapsed_entry = CollapsedEntry::ExternalFile(external_file.buffer_id);
1806                buffers_to_toggle.insert(external_file.buffer_id);
1807                if !self.collapsed_entries.remove(&collapsed_entry) {
1808                    self.collapsed_entries.insert(collapsed_entry);
1809                    fold = true;
1810                }
1811            }
1812            PanelEntry::FoldedDirs(FoldedDirsEntry {
1813                worktree_id,
1814                entries: dir_entries,
1815                ..
1816            }) => {
1817                if let Some(dir_entry) = dir_entries.first() {
1818                    let entry_id = dir_entry.id;
1819                    let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1820                    buffers_to_toggle
1821                        .extend(self.buffers_inside_directory(*worktree_id, dir_entry));
1822                    if self.collapsed_entries.remove(&collapsed_entry) {
1823                        self.project
1824                            .update(cx, |project, cx| {
1825                                project.expand_entry(*worktree_id, entry_id, cx)
1826                            })
1827                            .unwrap_or_else(|| Task::ready(Ok(())))
1828                            .detach_and_log_err(cx);
1829                    } else {
1830                        self.collapsed_entries.insert(collapsed_entry);
1831                        fold = true;
1832                    }
1833                }
1834            }
1835            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
1836                let collapsed_entry = CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id);
1837                if !self.collapsed_entries.remove(&collapsed_entry) {
1838                    self.collapsed_entries.insert(collapsed_entry);
1839                }
1840            }
1841            PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
1842                let collapsed_entry = CollapsedEntry::Outline(
1843                    outline.buffer_id,
1844                    outline.excerpt_id,
1845                    outline.outline.range.clone(),
1846                );
1847                if !self.collapsed_entries.remove(&collapsed_entry) {
1848                    self.collapsed_entries.insert(collapsed_entry);
1849                }
1850            }
1851            _ => {}
1852        }
1853
1854        active_editor.update(cx, |editor, cx| {
1855            buffers_to_toggle.retain(|buffer_id| {
1856                let folded = editor.is_buffer_folded(*buffer_id, cx);
1857                if fold { !folded } else { folded }
1858            });
1859        });
1860
1861        self.select_entry(entry.clone(), true, window, cx);
1862        if buffers_to_toggle.is_empty() {
1863            self.update_cached_entries(None, window, cx);
1864        } else {
1865            self.toggle_buffers_fold(buffers_to_toggle, fold, window, cx)
1866                .detach();
1867        }
1868    }
1869
1870    fn toggle_buffers_fold(
1871        &self,
1872        buffers: HashSet<BufferId>,
1873        fold: bool,
1874        window: &mut Window,
1875        cx: &mut Context<Self>,
1876    ) -> Task<()> {
1877        let Some(active_editor) = self.active_editor() else {
1878            return Task::ready(());
1879        };
1880        cx.spawn_in(window, async move |outline_panel, cx| {
1881            outline_panel
1882                .update_in(cx, |outline_panel, window, cx| {
1883                    active_editor.update(cx, |editor, cx| {
1884                        for buffer_id in buffers {
1885                            outline_panel
1886                                .preserve_selection_on_buffer_fold_toggles
1887                                .insert(buffer_id);
1888                            if fold {
1889                                editor.fold_buffer(buffer_id, cx);
1890                            } else {
1891                                editor.unfold_buffer(buffer_id, cx);
1892                            }
1893                        }
1894                    });
1895                    if let Some(selection) = outline_panel.selected_entry().cloned() {
1896                        outline_panel.scroll_editor_to_entry(&selection, false, false, window, cx);
1897                    }
1898                })
1899                .ok();
1900        })
1901    }
1902
1903    fn copy_path(
1904        &mut self,
1905        _: &zed_actions::workspace::CopyPath,
1906        _: &mut Window,
1907        cx: &mut Context<Self>,
1908    ) {
1909        if let Some(clipboard_text) = self
1910            .selected_entry()
1911            .and_then(|entry| self.abs_path(entry, cx))
1912            .map(|p| p.to_string_lossy().to_string())
1913        {
1914            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1915        }
1916    }
1917
1918    fn copy_relative_path(
1919        &mut self,
1920        _: &zed_actions::workspace::CopyRelativePath,
1921        _: &mut Window,
1922        cx: &mut Context<Self>,
1923    ) {
1924        if let Some(clipboard_text) = self
1925            .selected_entry()
1926            .and_then(|entry| match entry {
1927                PanelEntry::Fs(entry) => self.relative_path(entry, cx),
1928                PanelEntry::FoldedDirs(folded_dirs) => {
1929                    folded_dirs.entries.last().map(|entry| entry.path.clone())
1930                }
1931                PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1932            })
1933            .map(|p| p.to_string_lossy().to_string())
1934        {
1935            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1936        }
1937    }
1938
1939    fn reveal_in_finder(
1940        &mut self,
1941        _: &RevealInFileManager,
1942        _: &mut Window,
1943        cx: &mut Context<Self>,
1944    ) {
1945        if let Some(abs_path) = self
1946            .selected_entry()
1947            .and_then(|entry| self.abs_path(entry, cx))
1948        {
1949            cx.reveal_path(&abs_path);
1950        }
1951    }
1952
1953    fn open_in_terminal(
1954        &mut self,
1955        _: &OpenInTerminal,
1956        window: &mut Window,
1957        cx: &mut Context<Self>,
1958    ) {
1959        let selected_entry = self.selected_entry();
1960        let abs_path = selected_entry.and_then(|entry| self.abs_path(entry, cx));
1961        let working_directory = if let (
1962            Some(abs_path),
1963            Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))),
1964        ) = (&abs_path, selected_entry)
1965        {
1966            abs_path.parent().map(|p| p.to_owned())
1967        } else {
1968            abs_path
1969        };
1970
1971        if let Some(working_directory) = working_directory {
1972            window.dispatch_action(
1973                workspace::OpenTerminal { working_directory }.boxed_clone(),
1974                cx,
1975            )
1976        }
1977    }
1978
1979    fn reveal_entry_for_selection(
1980        &mut self,
1981        editor: Entity<Editor>,
1982        window: &mut Window,
1983        cx: &mut Context<Self>,
1984    ) {
1985        if !self.active
1986            || !OutlinePanelSettings::get_global(cx).auto_reveal_entries
1987            || self.focus_handle.contains_focused(window, cx)
1988        {
1989            return;
1990        }
1991        let project = self.project.clone();
1992        self.reveal_selection_task = cx.spawn_in(window, async move |outline_panel, cx| {
1993            cx.background_executor().timer(UPDATE_DEBOUNCE).await;
1994            let entry_with_selection =
1995                outline_panel.update_in(cx, |outline_panel, window, cx| {
1996                    outline_panel.location_for_editor_selection(&editor, window, cx)
1997                })?;
1998            let Some(entry_with_selection) = entry_with_selection else {
1999                outline_panel.update(cx, |outline_panel, cx| {
2000                    outline_panel.selected_entry = SelectedEntry::None;
2001                    cx.notify();
2002                })?;
2003                return Ok(());
2004            };
2005            let related_buffer_entry = match &entry_with_selection {
2006                PanelEntry::Fs(FsEntry::File(FsEntryFile {
2007                    worktree_id,
2008                    buffer_id,
2009                    ..
2010                })) => project.update(cx, |project, cx| {
2011                    let entry_id = project
2012                        .buffer_for_id(*buffer_id, cx)
2013                        .and_then(|buffer| buffer.read(cx).entry_id(cx));
2014                    project
2015                        .worktree_for_id(*worktree_id, cx)
2016                        .zip(entry_id)
2017                        .and_then(|(worktree, entry_id)| {
2018                            let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
2019                            Some((worktree, entry))
2020                        })
2021                })?,
2022                PanelEntry::Outline(outline_entry) => {
2023                    let (buffer_id, excerpt_id) = outline_entry.ids();
2024                    outline_panel.update(cx, |outline_panel, cx| {
2025                        outline_panel
2026                            .collapsed_entries
2027                            .remove(&CollapsedEntry::ExternalFile(buffer_id));
2028                        outline_panel
2029                            .collapsed_entries
2030                            .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
2031                        let project = outline_panel.project.read(cx);
2032                        let entry_id = project
2033                            .buffer_for_id(buffer_id, cx)
2034                            .and_then(|buffer| buffer.read(cx).entry_id(cx));
2035
2036                        entry_id.and_then(|entry_id| {
2037                            project
2038                                .worktree_for_entry(entry_id, cx)
2039                                .and_then(|worktree| {
2040                                    let worktree_id = worktree.read(cx).id();
2041                                    outline_panel
2042                                        .collapsed_entries
2043                                        .remove(&CollapsedEntry::File(worktree_id, buffer_id));
2044                                    let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
2045                                    Some((worktree, entry))
2046                                })
2047                        })
2048                    })?
2049                }
2050                PanelEntry::Fs(FsEntry::ExternalFile(..)) => None,
2051                PanelEntry::Search(SearchEntry { match_range, .. }) => match_range
2052                    .start
2053                    .buffer_id
2054                    .or(match_range.end.buffer_id)
2055                    .map(|buffer_id| {
2056                        outline_panel.update(cx, |outline_panel, cx| {
2057                            outline_panel
2058                                .collapsed_entries
2059                                .remove(&CollapsedEntry::ExternalFile(buffer_id));
2060                            let project = project.read(cx);
2061                            let entry_id = project
2062                                .buffer_for_id(buffer_id, cx)
2063                                .and_then(|buffer| buffer.read(cx).entry_id(cx));
2064
2065                            entry_id.and_then(|entry_id| {
2066                                project
2067                                    .worktree_for_entry(entry_id, cx)
2068                                    .and_then(|worktree| {
2069                                        let worktree_id = worktree.read(cx).id();
2070                                        outline_panel
2071                                            .collapsed_entries
2072                                            .remove(&CollapsedEntry::File(worktree_id, buffer_id));
2073                                        let entry =
2074                                            worktree.read(cx).entry_for_id(entry_id)?.clone();
2075                                        Some((worktree, entry))
2076                                    })
2077                            })
2078                        })
2079                    })
2080                    .transpose()?
2081                    .flatten(),
2082                _ => return anyhow::Ok(()),
2083            };
2084            if let Some((worktree, buffer_entry)) = related_buffer_entry {
2085                outline_panel.update(cx, |outline_panel, cx| {
2086                    let worktree_id = worktree.read(cx).id();
2087                    let mut dirs_to_expand = Vec::new();
2088                    {
2089                        let mut traversal = worktree.read(cx).traverse_from_path(
2090                            true,
2091                            true,
2092                            true,
2093                            buffer_entry.path.as_ref(),
2094                        );
2095                        let mut current_entry = buffer_entry;
2096                        loop {
2097                            if current_entry.is_dir()
2098                                && outline_panel
2099                                    .collapsed_entries
2100                                    .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
2101                            {
2102                                dirs_to_expand.push(current_entry.id);
2103                            }
2104
2105                            if traversal.back_to_parent()
2106                                && let Some(parent_entry) = traversal.entry()
2107                            {
2108                                current_entry = parent_entry.clone();
2109                                continue;
2110                            }
2111                            break;
2112                        }
2113                    }
2114                    for dir_to_expand in dirs_to_expand {
2115                        project
2116                            .update(cx, |project, cx| {
2117                                project.expand_entry(worktree_id, dir_to_expand, cx)
2118                            })
2119                            .unwrap_or_else(|| Task::ready(Ok(())))
2120                            .detach_and_log_err(cx)
2121                    }
2122                })?
2123            }
2124
2125            outline_panel.update_in(cx, |outline_panel, window, cx| {
2126                outline_panel.select_entry(entry_with_selection, false, window, cx);
2127                outline_panel.update_cached_entries(None, window, cx);
2128            })?;
2129
2130            anyhow::Ok(())
2131        });
2132    }
2133
2134    fn render_excerpt(
2135        &self,
2136        excerpt: &OutlineEntryExcerpt,
2137        depth: usize,
2138        window: &mut Window,
2139        cx: &mut Context<OutlinePanel>,
2140    ) -> Option<Stateful<Div>> {
2141        let item_id = ElementId::from(excerpt.id.to_proto() as usize);
2142        let is_active = match self.selected_entry() {
2143            Some(PanelEntry::Outline(OutlineEntry::Excerpt(selected_excerpt))) => {
2144                selected_excerpt.buffer_id == excerpt.buffer_id && selected_excerpt.id == excerpt.id
2145            }
2146            _ => false,
2147        };
2148        let has_outlines = self
2149            .excerpts
2150            .get(&excerpt.buffer_id)
2151            .and_then(|excerpts| match &excerpts.get(&excerpt.id)?.outlines {
2152                ExcerptOutlines::Outlines(outlines) => Some(outlines),
2153                ExcerptOutlines::Invalidated(outlines) => Some(outlines),
2154                ExcerptOutlines::NotFetched => None,
2155            })
2156            .is_some_and(|outlines| !outlines.is_empty());
2157        let is_expanded = !self
2158            .collapsed_entries
2159            .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id));
2160        let color = entry_label_color(is_active);
2161        let icon = if has_outlines {
2162            FileIcons::get_chevron_icon(is_expanded, cx)
2163                .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2164        } else {
2165            None
2166        }
2167        .unwrap_or_else(empty_icon);
2168
2169        let label = self.excerpt_label(excerpt.buffer_id, &excerpt.range, cx)?;
2170        let label_element = Label::new(label)
2171            .single_line()
2172            .color(color)
2173            .into_any_element();
2174
2175        Some(self.entry_element(
2176            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())),
2177            item_id,
2178            depth,
2179            icon,
2180            is_active,
2181            label_element,
2182            window,
2183            cx,
2184        ))
2185    }
2186
2187    fn excerpt_label(
2188        &self,
2189        buffer_id: BufferId,
2190        range: &ExcerptRange<language::Anchor>,
2191        cx: &App,
2192    ) -> Option<String> {
2193        let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
2194        let excerpt_range = range.context.to_point(&buffer_snapshot);
2195        Some(format!(
2196            "Lines {}- {}",
2197            excerpt_range.start.row + 1,
2198            excerpt_range.end.row + 1,
2199        ))
2200    }
2201
2202    fn render_outline(
2203        &self,
2204        outline: &OutlineEntryOutline,
2205        depth: usize,
2206        string_match: Option<&StringMatch>,
2207        window: &mut Window,
2208        cx: &mut Context<Self>,
2209    ) -> Stateful<Div> {
2210        let item_id = ElementId::from(SharedString::from(format!(
2211            "{:?}|{:?}{:?}|{:?}",
2212            outline.buffer_id, outline.excerpt_id, outline.outline.range, &outline.outline.text,
2213        )));
2214
2215        let label_element = outline::render_item(
2216            &outline.outline,
2217            string_match
2218                .map(|string_match| string_match.ranges().collect::<Vec<_>>())
2219                .unwrap_or_default(),
2220            cx,
2221        )
2222        .into_any_element();
2223
2224        let is_active = match self.selected_entry() {
2225            Some(PanelEntry::Outline(OutlineEntry::Outline(selected))) => {
2226                outline == selected && outline.outline == selected.outline
2227            }
2228            _ => false,
2229        };
2230
2231        let has_children = self
2232            .outline_children_cache
2233            .get(&outline.buffer_id)
2234            .and_then(|children_map| {
2235                let key = (outline.outline.range.clone(), outline.outline.depth);
2236                children_map.get(&key)
2237            })
2238            .copied()
2239            .unwrap_or(false);
2240        let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Outline(
2241            outline.buffer_id,
2242            outline.excerpt_id,
2243            outline.outline.range.clone(),
2244        ));
2245
2246        let icon = if has_children {
2247            FileIcons::get_chevron_icon(is_expanded, cx)
2248                .map(|icon_path| {
2249                    Icon::from_path(icon_path)
2250                        .color(entry_label_color(is_active))
2251                        .into_any_element()
2252                })
2253                .unwrap_or_else(empty_icon)
2254        } else {
2255            empty_icon()
2256        };
2257
2258        self.entry_element(
2259            PanelEntry::Outline(OutlineEntry::Outline(outline.clone())),
2260            item_id,
2261            depth,
2262            icon,
2263            is_active,
2264            label_element,
2265            window,
2266            cx,
2267        )
2268    }
2269
2270    fn render_entry(
2271        &self,
2272        rendered_entry: &FsEntry,
2273        depth: usize,
2274        string_match: Option<&StringMatch>,
2275        window: &mut Window,
2276        cx: &mut Context<Self>,
2277    ) -> Stateful<Div> {
2278        let settings = OutlinePanelSettings::get_global(cx);
2279        let is_active = match self.selected_entry() {
2280            Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry,
2281            _ => false,
2282        };
2283        let (item_id, label_element, icon) = match rendered_entry {
2284            FsEntry::File(FsEntryFile {
2285                worktree_id, entry, ..
2286            }) => {
2287                let name = self.entry_name(worktree_id, entry, cx);
2288                let color =
2289                    entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active);
2290                let icon = if settings.file_icons {
2291                    FileIcons::get_icon(&entry.path, cx)
2292                        .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2293                } else {
2294                    None
2295                };
2296                (
2297                    ElementId::from(entry.id.to_proto() as usize),
2298                    HighlightedLabel::new(
2299                        name,
2300                        string_match
2301                            .map(|string_match| string_match.positions.clone())
2302                            .unwrap_or_default(),
2303                    )
2304                    .color(color)
2305                    .into_any_element(),
2306                    icon.unwrap_or_else(empty_icon),
2307                )
2308            }
2309            FsEntry::Directory(directory) => {
2310                let name = self.entry_name(&directory.worktree_id, &directory.entry, cx);
2311
2312                let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Dir(
2313                    directory.worktree_id,
2314                    directory.entry.id,
2315                ));
2316                let color = entry_git_aware_label_color(
2317                    directory.entry.git_summary,
2318                    directory.entry.is_ignored,
2319                    is_active,
2320                );
2321                let icon = if settings.folder_icons {
2322                    FileIcons::get_folder_icon(is_expanded, cx)
2323                } else {
2324                    FileIcons::get_chevron_icon(is_expanded, cx)
2325                }
2326                .map(Icon::from_path)
2327                .map(|icon| icon.color(color).into_any_element());
2328                (
2329                    ElementId::from(directory.entry.id.to_proto() as usize),
2330                    HighlightedLabel::new(
2331                        name,
2332                        string_match
2333                            .map(|string_match| string_match.positions.clone())
2334                            .unwrap_or_default(),
2335                    )
2336                    .color(color)
2337                    .into_any_element(),
2338                    icon.unwrap_or_else(empty_icon),
2339                )
2340            }
2341            FsEntry::ExternalFile(external_file) => {
2342                let color = entry_label_color(is_active);
2343                let (icon, name) = match self.buffer_snapshot_for_id(external_file.buffer_id, cx) {
2344                    Some(buffer_snapshot) => match buffer_snapshot.file() {
2345                        Some(file) => {
2346                            let path = file.path();
2347                            let icon = if settings.file_icons {
2348                                FileIcons::get_icon(path.as_ref(), cx)
2349                            } else {
2350                                None
2351                            }
2352                            .map(Icon::from_path)
2353                            .map(|icon| icon.color(color).into_any_element());
2354                            (icon, file_name(path.as_ref()))
2355                        }
2356                        None => (None, "Untitled".to_string()),
2357                    },
2358                    None => (None, "Unknown buffer".to_string()),
2359                };
2360                (
2361                    ElementId::from(external_file.buffer_id.to_proto() as usize),
2362                    HighlightedLabel::new(
2363                        name,
2364                        string_match
2365                            .map(|string_match| string_match.positions.clone())
2366                            .unwrap_or_default(),
2367                    )
2368                    .color(color)
2369                    .into_any_element(),
2370                    icon.unwrap_or_else(empty_icon),
2371                )
2372            }
2373        };
2374
2375        self.entry_element(
2376            PanelEntry::Fs(rendered_entry.clone()),
2377            item_id,
2378            depth,
2379            icon,
2380            is_active,
2381            label_element,
2382            window,
2383            cx,
2384        )
2385    }
2386
2387    fn render_folded_dirs(
2388        &self,
2389        folded_dir: &FoldedDirsEntry,
2390        depth: usize,
2391        string_match: Option<&StringMatch>,
2392        window: &mut Window,
2393        cx: &mut Context<OutlinePanel>,
2394    ) -> Stateful<Div> {
2395        let settings = OutlinePanelSettings::get_global(cx);
2396        let is_active = match self.selected_entry() {
2397            Some(PanelEntry::FoldedDirs(selected_dirs)) => {
2398                selected_dirs.worktree_id == folded_dir.worktree_id
2399                    && selected_dirs.entries == folded_dir.entries
2400            }
2401            _ => false,
2402        };
2403        let (item_id, label_element, icon) = {
2404            let name = self.dir_names_string(&folded_dir.entries, folded_dir.worktree_id, cx);
2405
2406            let is_expanded = folded_dir.entries.iter().all(|dir| {
2407                !self
2408                    .collapsed_entries
2409                    .contains(&CollapsedEntry::Dir(folded_dir.worktree_id, dir.id))
2410            });
2411            let is_ignored = folded_dir.entries.iter().any(|entry| entry.is_ignored);
2412            let git_status = folded_dir
2413                .entries
2414                .first()
2415                .map(|entry| entry.git_summary)
2416                .unwrap_or_default();
2417            let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
2418            let icon = if settings.folder_icons {
2419                FileIcons::get_folder_icon(is_expanded, cx)
2420            } else {
2421                FileIcons::get_chevron_icon(is_expanded, cx)
2422            }
2423            .map(Icon::from_path)
2424            .map(|icon| icon.color(color).into_any_element());
2425            (
2426                ElementId::from(
2427                    folded_dir
2428                        .entries
2429                        .last()
2430                        .map(|entry| entry.id.to_proto())
2431                        .unwrap_or_else(|| folded_dir.worktree_id.to_proto())
2432                        as usize,
2433                ),
2434                HighlightedLabel::new(
2435                    name,
2436                    string_match
2437                        .map(|string_match| string_match.positions.clone())
2438                        .unwrap_or_default(),
2439                )
2440                .color(color)
2441                .into_any_element(),
2442                icon.unwrap_or_else(empty_icon),
2443            )
2444        };
2445
2446        self.entry_element(
2447            PanelEntry::FoldedDirs(folded_dir.clone()),
2448            item_id,
2449            depth,
2450            icon,
2451            is_active,
2452            label_element,
2453            window,
2454            cx,
2455        )
2456    }
2457
2458    fn render_search_match(
2459        &mut self,
2460        multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
2461        match_range: &Range<editor::Anchor>,
2462        render_data: &Arc<OnceLock<SearchData>>,
2463        kind: SearchKind,
2464        depth: usize,
2465        string_match: Option<&StringMatch>,
2466        window: &mut Window,
2467        cx: &mut Context<Self>,
2468    ) -> Option<Stateful<Div>> {
2469        let search_data = match render_data.get() {
2470            Some(search_data) => search_data,
2471            None => {
2472                if let ItemsDisplayMode::Search(search_state) = &mut self.mode
2473                    && let Some(multi_buffer_snapshot) = multi_buffer_snapshot
2474                {
2475                    search_state
2476                        .highlight_search_match_tx
2477                        .try_send(HighlightArguments {
2478                            multi_buffer_snapshot: multi_buffer_snapshot.clone(),
2479                            match_range: match_range.clone(),
2480                            search_data: Arc::clone(render_data),
2481                        })
2482                        .ok();
2483                }
2484                return None;
2485            }
2486        };
2487        let search_matches = string_match
2488            .iter()
2489            .flat_map(|string_match| string_match.ranges())
2490            .collect::<Vec<_>>();
2491        let match_ranges = if search_matches.is_empty() {
2492            &search_data.search_match_indices
2493        } else {
2494            &search_matches
2495        };
2496        let label_element = outline::render_item(
2497            &OutlineItem {
2498                depth,
2499                annotation_range: None,
2500                range: search_data.context_range.clone(),
2501                text: search_data.context_text.clone(),
2502                highlight_ranges: search_data
2503                    .highlights_data
2504                    .get()
2505                    .cloned()
2506                    .unwrap_or_default(),
2507                name_ranges: search_data.search_match_indices.clone(),
2508                body_range: Some(search_data.context_range.clone()),
2509            },
2510            match_ranges.iter().cloned(),
2511            cx,
2512        );
2513        let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK);
2514        let entire_label = h_flex()
2515            .justify_center()
2516            .p_0()
2517            .when(search_data.truncated_left, |parent| {
2518                parent.child(truncated_contents_label())
2519            })
2520            .child(label_element)
2521            .when(search_data.truncated_right, |parent| {
2522                parent.child(truncated_contents_label())
2523            })
2524            .into_any_element();
2525
2526        let is_active = match self.selected_entry() {
2527            Some(PanelEntry::Search(SearchEntry {
2528                match_range: selected_match_range,
2529                ..
2530            })) => match_range == selected_match_range,
2531            _ => false,
2532        };
2533        Some(self.entry_element(
2534            PanelEntry::Search(SearchEntry {
2535                kind,
2536                match_range: match_range.clone(),
2537                render_data: render_data.clone(),
2538            }),
2539            ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
2540            depth,
2541            empty_icon(),
2542            is_active,
2543            entire_label,
2544            window,
2545            cx,
2546        ))
2547    }
2548
2549    fn entry_element(
2550        &self,
2551        rendered_entry: PanelEntry,
2552        item_id: ElementId,
2553        depth: usize,
2554        icon_element: AnyElement,
2555        is_active: bool,
2556        label_element: gpui::AnyElement,
2557        window: &mut Window,
2558        cx: &mut Context<OutlinePanel>,
2559    ) -> Stateful<Div> {
2560        let settings = OutlinePanelSettings::get_global(cx);
2561        div()
2562            .text_ui(cx)
2563            .id(item_id.clone())
2564            .on_click({
2565                let clicked_entry = rendered_entry.clone();
2566                cx.listener(move |outline_panel, event: &gpui::ClickEvent, window, cx| {
2567                    if event.is_right_click() || event.first_focus() {
2568                        return;
2569                    }
2570
2571                    let change_focus = event.click_count() > 1;
2572                    outline_panel.toggle_expanded(&clicked_entry, window, cx);
2573
2574                    outline_panel.scroll_editor_to_entry(
2575                        &clicked_entry,
2576                        true,
2577                        change_focus,
2578                        window,
2579                        cx,
2580                    );
2581                })
2582            })
2583            .cursor_pointer()
2584            .child(
2585                ListItem::new(item_id)
2586                    .indent_level(depth)
2587                    .indent_step_size(px(settings.indent_size))
2588                    .toggle_state(is_active)
2589                    .child(
2590                        h_flex()
2591                            .child(h_flex().w(px(16.)).justify_center().child(icon_element))
2592                            .child(h_flex().h_6().child(label_element).ml_1()),
2593                    )
2594                    .on_secondary_mouse_down(cx.listener(
2595                        move |outline_panel, event: &MouseDownEvent, window, cx| {
2596                            // Stop propagation to prevent the catch-all context menu for the project
2597                            // panel from being deployed.
2598                            cx.stop_propagation();
2599                            outline_panel.deploy_context_menu(
2600                                event.position,
2601                                rendered_entry.clone(),
2602                                window,
2603                                cx,
2604                            )
2605                        },
2606                    )),
2607            )
2608            .border_1()
2609            .border_r_2()
2610            .rounded_none()
2611            .hover(|style| {
2612                if is_active {
2613                    style
2614                } else {
2615                    let hover_color = cx.theme().colors().ghost_element_hover;
2616                    style.bg(hover_color).border_color(hover_color)
2617                }
2618            })
2619            .when(
2620                is_active && self.focus_handle.contains_focused(window, cx),
2621                |div| div.border_color(Color::Selected.color(cx)),
2622            )
2623    }
2624
2625    fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String {
2626        match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
2627            Some(worktree) => {
2628                let worktree = worktree.read(cx);
2629                match worktree.snapshot().root_entry() {
2630                    Some(root_entry) => {
2631                        if root_entry.id == entry.id {
2632                            file_name(worktree.abs_path().as_ref())
2633                        } else {
2634                            let path = worktree.absolutize(entry.path.as_ref()).ok();
2635                            let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2636                            file_name(path)
2637                        }
2638                    }
2639                    None => {
2640                        let path = worktree.absolutize(entry.path.as_ref()).ok();
2641                        let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2642                        file_name(path)
2643                    }
2644                }
2645            }
2646            None => file_name(entry.path.as_ref()),
2647        }
2648    }
2649
2650    fn update_fs_entries(
2651        &mut self,
2652        active_editor: Entity<Editor>,
2653        debounce: Option<Duration>,
2654        window: &mut Window,
2655        cx: &mut Context<Self>,
2656    ) {
2657        if !self.active {
2658            return;
2659        }
2660
2661        let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2662        let active_multi_buffer = active_editor.read(cx).buffer().clone();
2663        let new_entries = self.new_entries_for_fs_update.clone();
2664        let repo_snapshots = self.project.update(cx, |project, cx| {
2665            project.git_store().read(cx).repo_snapshots(cx)
2666        });
2667        self.updating_fs_entries = true;
2668        self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
2669            if let Some(debounce) = debounce {
2670                cx.background_executor().timer(debounce).await;
2671            }
2672
2673            let mut new_collapsed_entries = HashSet::default();
2674            let mut new_unfolded_dirs = HashMap::default();
2675            let mut root_entries = HashSet::default();
2676            let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
2677            let Ok(buffer_excerpts) = outline_panel.update(cx, |outline_panel, cx| {
2678                let git_store = outline_panel.project.read(cx).git_store().clone();
2679                new_collapsed_entries = outline_panel.collapsed_entries.clone();
2680                new_unfolded_dirs = outline_panel.unfolded_dirs.clone();
2681                let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
2682
2683                multi_buffer_snapshot.excerpts().fold(
2684                    HashMap::default(),
2685                    |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
2686                        let buffer_id = buffer_snapshot.remote_id();
2687                        let file = File::from_dyn(buffer_snapshot.file());
2688                        let entry_id = file.and_then(|file| file.project_entry_id(cx));
2689                        let worktree = file.map(|file| file.worktree.read(cx).snapshot());
2690                        let is_new = new_entries.contains(&excerpt_id)
2691                            || !outline_panel.excerpts.contains_key(&buffer_id);
2692                        let is_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
2693                        let status = git_store
2694                            .read(cx)
2695                            .repository_and_path_for_buffer_id(buffer_id, cx)
2696                            .and_then(|(repo, path)| {
2697                                Some(repo.read(cx).status_for_path(&path)?.status)
2698                            });
2699                        buffer_excerpts
2700                            .entry(buffer_id)
2701                            .or_insert_with(|| {
2702                                (is_new, is_folded, Vec::new(), entry_id, worktree, status)
2703                            })
2704                            .2
2705                            .push(excerpt_id);
2706
2707                        let outlines = match outline_panel
2708                            .excerpts
2709                            .get(&buffer_id)
2710                            .and_then(|excerpts| excerpts.get(&excerpt_id))
2711                        {
2712                            Some(old_excerpt) => match &old_excerpt.outlines {
2713                                ExcerptOutlines::Outlines(outlines) => {
2714                                    ExcerptOutlines::Outlines(outlines.clone())
2715                                }
2716                                ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
2717                                ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
2718                            },
2719                            None => ExcerptOutlines::NotFetched,
2720                        };
2721                        new_excerpts.entry(buffer_id).or_default().insert(
2722                            excerpt_id,
2723                            Excerpt {
2724                                range: excerpt_range,
2725                                outlines,
2726                            },
2727                        );
2728                        buffer_excerpts
2729                    },
2730                )
2731            }) else {
2732                return;
2733            };
2734
2735            let Some((
2736                new_collapsed_entries,
2737                new_unfolded_dirs,
2738                new_fs_entries,
2739                new_depth_map,
2740                new_children_count,
2741            )) = cx
2742                .background_spawn(async move {
2743                    let mut processed_external_buffers = HashSet::default();
2744                    let mut new_worktree_entries =
2745                        BTreeMap::<WorktreeId, HashMap<ProjectEntryId, GitEntry>>::default();
2746                    let mut worktree_excerpts = HashMap::<
2747                        WorktreeId,
2748                        HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
2749                    >::default();
2750                    let mut external_excerpts = HashMap::default();
2751
2752                    for (buffer_id, (is_new, is_folded, excerpts, entry_id, worktree, status)) in
2753                        buffer_excerpts
2754                    {
2755                        if is_folded {
2756                            match &worktree {
2757                                Some(worktree) => {
2758                                    new_collapsed_entries
2759                                        .insert(CollapsedEntry::File(worktree.id(), buffer_id));
2760                                }
2761                                None => {
2762                                    new_collapsed_entries
2763                                        .insert(CollapsedEntry::ExternalFile(buffer_id));
2764                                }
2765                            }
2766                        } else if is_new {
2767                            match &worktree {
2768                                Some(worktree) => {
2769                                    new_collapsed_entries
2770                                        .remove(&CollapsedEntry::File(worktree.id(), buffer_id));
2771                                }
2772                                None => {
2773                                    new_collapsed_entries
2774                                        .remove(&CollapsedEntry::ExternalFile(buffer_id));
2775                                }
2776                            }
2777                        }
2778
2779                        if let Some(worktree) = worktree {
2780                            let worktree_id = worktree.id();
2781                            let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
2782
2783                            match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
2784                                Some(entry) => {
2785                                    let entry = GitEntry {
2786                                        git_summary: status
2787                                            .map(|status| status.summary())
2788                                            .unwrap_or_default(),
2789                                        entry,
2790                                    };
2791                                    let mut traversal = GitTraversal::new(
2792                                        &repo_snapshots,
2793                                        worktree.traverse_from_path(
2794                                            true,
2795                                            true,
2796                                            true,
2797                                            entry.path.as_ref(),
2798                                        ),
2799                                    );
2800
2801                                    let mut entries_to_add = HashMap::default();
2802                                    worktree_excerpts
2803                                        .entry(worktree_id)
2804                                        .or_default()
2805                                        .insert(entry.id, (buffer_id, excerpts));
2806                                    let mut current_entry = entry;
2807                                    loop {
2808                                        if current_entry.is_dir() {
2809                                            let is_root =
2810                                                worktree.root_entry().map(|entry| entry.id)
2811                                                    == Some(current_entry.id);
2812                                            if is_root {
2813                                                root_entries.insert(current_entry.id);
2814                                                if auto_fold_dirs {
2815                                                    unfolded_dirs.insert(current_entry.id);
2816                                                }
2817                                            }
2818                                            if is_new {
2819                                                new_collapsed_entries.remove(&CollapsedEntry::Dir(
2820                                                    worktree_id,
2821                                                    current_entry.id,
2822                                                ));
2823                                            }
2824                                        }
2825
2826                                        let new_entry_added = entries_to_add
2827                                            .insert(current_entry.id, current_entry)
2828                                            .is_none();
2829                                        if new_entry_added
2830                                            && traversal.back_to_parent()
2831                                            && let Some(parent_entry) = traversal.entry()
2832                                        {
2833                                            current_entry = parent_entry.to_owned();
2834                                            continue;
2835                                        }
2836                                        break;
2837                                    }
2838                                    new_worktree_entries
2839                                        .entry(worktree_id)
2840                                        .or_insert_with(HashMap::default)
2841                                        .extend(entries_to_add);
2842                                }
2843                                None => {
2844                                    if processed_external_buffers.insert(buffer_id) {
2845                                        external_excerpts
2846                                            .entry(buffer_id)
2847                                            .or_insert_with(Vec::new)
2848                                            .extend(excerpts);
2849                                    }
2850                                }
2851                            }
2852                        } else if processed_external_buffers.insert(buffer_id) {
2853                            external_excerpts
2854                                .entry(buffer_id)
2855                                .or_insert_with(Vec::new)
2856                                .extend(excerpts);
2857                        }
2858                    }
2859
2860                    let mut new_children_count =
2861                        HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
2862
2863                    let worktree_entries = new_worktree_entries
2864                        .into_iter()
2865                        .map(|(worktree_id, entries)| {
2866                            let mut entries = entries.into_values().collect::<Vec<_>>();
2867                            entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
2868                            (worktree_id, entries)
2869                        })
2870                        .flat_map(|(worktree_id, entries)| {
2871                            {
2872                                entries
2873                                    .into_iter()
2874                                    .filter_map(|entry| {
2875                                        if auto_fold_dirs && let Some(parent) = entry.path.parent()
2876                                        {
2877                                            let children = new_children_count
2878                                                .entry(worktree_id)
2879                                                .or_default()
2880                                                .entry(Arc::from(parent))
2881                                                .or_default();
2882                                            if entry.is_dir() {
2883                                                children.dirs += 1;
2884                                            } else {
2885                                                children.files += 1;
2886                                            }
2887                                        }
2888
2889                                        if entry.is_dir() {
2890                                            Some(FsEntry::Directory(FsEntryDirectory {
2891                                                worktree_id,
2892                                                entry,
2893                                            }))
2894                                        } else {
2895                                            let (buffer_id, excerpts) = worktree_excerpts
2896                                                .get_mut(&worktree_id)
2897                                                .and_then(|worktree_excerpts| {
2898                                                    worktree_excerpts.remove(&entry.id)
2899                                                })?;
2900                                            Some(FsEntry::File(FsEntryFile {
2901                                                worktree_id,
2902                                                buffer_id,
2903                                                entry,
2904                                                excerpts,
2905                                            }))
2906                                        }
2907                                    })
2908                                    .collect::<Vec<_>>()
2909                            }
2910                        })
2911                        .collect::<Vec<_>>();
2912
2913                    let mut visited_dirs = Vec::new();
2914                    let mut new_depth_map = HashMap::default();
2915                    let new_visible_entries = external_excerpts
2916                        .into_iter()
2917                        .sorted_by_key(|(id, _)| *id)
2918                        .map(|(buffer_id, excerpts)| {
2919                            FsEntry::ExternalFile(FsEntryExternalFile {
2920                                buffer_id,
2921                                excerpts,
2922                            })
2923                        })
2924                        .chain(worktree_entries)
2925                        .filter(|visible_item| {
2926                            match visible_item {
2927                                FsEntry::Directory(directory) => {
2928                                    let parent_id = back_to_common_visited_parent(
2929                                        &mut visited_dirs,
2930                                        &directory.worktree_id,
2931                                        &directory.entry,
2932                                    );
2933
2934                                    let mut depth = 0;
2935                                    if !root_entries.contains(&directory.entry.id) {
2936                                        if auto_fold_dirs {
2937                                            let children = new_children_count
2938                                                .get(&directory.worktree_id)
2939                                                .and_then(|children_count| {
2940                                                    children_count.get(&directory.entry.path)
2941                                                })
2942                                                .copied()
2943                                                .unwrap_or_default();
2944
2945                                            if !children.may_be_fold_part()
2946                                                || (children.dirs == 0
2947                                                    && visited_dirs
2948                                                        .last()
2949                                                        .map(|(parent_dir_id, _)| {
2950                                                            new_unfolded_dirs
2951                                                                .get(&directory.worktree_id)
2952                                                                .is_none_or(|unfolded_dirs| {
2953                                                                    unfolded_dirs
2954                                                                        .contains(parent_dir_id)
2955                                                                })
2956                                                        })
2957                                                        .unwrap_or(true))
2958                                            {
2959                                                new_unfolded_dirs
2960                                                    .entry(directory.worktree_id)
2961                                                    .or_default()
2962                                                    .insert(directory.entry.id);
2963                                            }
2964                                        }
2965
2966                                        depth = parent_id
2967                                            .and_then(|(worktree_id, id)| {
2968                                                new_depth_map.get(&(worktree_id, id)).copied()
2969                                            })
2970                                            .unwrap_or(0)
2971                                            + 1;
2972                                    };
2973                                    visited_dirs
2974                                        .push((directory.entry.id, directory.entry.path.clone()));
2975                                    new_depth_map
2976                                        .insert((directory.worktree_id, directory.entry.id), depth);
2977                                }
2978                                FsEntry::File(FsEntryFile {
2979                                    worktree_id,
2980                                    entry: file_entry,
2981                                    ..
2982                                }) => {
2983                                    let parent_id = back_to_common_visited_parent(
2984                                        &mut visited_dirs,
2985                                        worktree_id,
2986                                        file_entry,
2987                                    );
2988                                    let depth = if root_entries.contains(&file_entry.id) {
2989                                        0
2990                                    } else {
2991                                        parent_id
2992                                            .and_then(|(worktree_id, id)| {
2993                                                new_depth_map.get(&(worktree_id, id)).copied()
2994                                            })
2995                                            .unwrap_or(0)
2996                                            + 1
2997                                    };
2998                                    new_depth_map.insert((*worktree_id, file_entry.id), depth);
2999                                }
3000                                FsEntry::ExternalFile(..) => {
3001                                    visited_dirs.clear();
3002                                }
3003                            }
3004
3005                            true
3006                        })
3007                        .collect::<Vec<_>>();
3008
3009                    anyhow::Ok((
3010                        new_collapsed_entries,
3011                        new_unfolded_dirs,
3012                        new_visible_entries,
3013                        new_depth_map,
3014                        new_children_count,
3015                    ))
3016                })
3017                .await
3018                .log_err()
3019            else {
3020                return;
3021            };
3022
3023            outline_panel
3024                .update_in(cx, |outline_panel, window, cx| {
3025                    outline_panel.updating_fs_entries = false;
3026                    outline_panel.new_entries_for_fs_update.clear();
3027                    outline_panel.excerpts = new_excerpts;
3028                    outline_panel.collapsed_entries = new_collapsed_entries;
3029                    outline_panel.unfolded_dirs = new_unfolded_dirs;
3030                    outline_panel.fs_entries = new_fs_entries;
3031                    outline_panel.fs_entries_depth = new_depth_map;
3032                    outline_panel.fs_children_count = new_children_count;
3033                    outline_panel.update_non_fs_items(window, cx);
3034
3035                    // Only update cached entries if we don't have outlines to fetch
3036                    // If we do have outlines to fetch, let fetch_outdated_outlines handle the update
3037                    if outline_panel.excerpt_fetch_ranges(cx).is_empty() {
3038                        outline_panel.update_cached_entries(debounce, window, cx);
3039                    }
3040
3041                    cx.notify();
3042                })
3043                .ok();
3044        });
3045    }
3046
3047    fn replace_active_editor(
3048        &mut self,
3049        new_active_item: Box<dyn ItemHandle>,
3050        new_active_editor: Entity<Editor>,
3051        window: &mut Window,
3052        cx: &mut Context<Self>,
3053    ) {
3054        self.clear_previous(window, cx);
3055
3056        let default_expansion_depth =
3057            OutlinePanelSettings::get_global(cx).expand_outlines_with_depth;
3058        // We'll apply the expansion depth after outlines are loaded
3059        self.pending_default_expansion_depth = Some(default_expansion_depth);
3060
3061        let buffer_search_subscription = cx.subscribe_in(
3062            &new_active_editor,
3063            window,
3064            |outline_panel: &mut Self,
3065             _,
3066             e: &SearchEvent,
3067             window: &mut Window,
3068             cx: &mut Context<Self>| {
3069                if matches!(e, SearchEvent::MatchesInvalidated) {
3070                    let update_cached_items = outline_panel.update_search_matches(window, cx);
3071                    if update_cached_items {
3072                        outline_panel.selected_entry.invalidate();
3073                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
3074                    }
3075                };
3076                outline_panel.autoscroll(cx);
3077            },
3078        );
3079        self.active_item = Some(ActiveItem {
3080            _buffer_search_subscription: buffer_search_subscription,
3081            _editor_subscription: subscribe_for_editor_events(&new_active_editor, window, cx),
3082            item_handle: new_active_item.downgrade_item(),
3083            active_editor: new_active_editor.downgrade(),
3084        });
3085        self.new_entries_for_fs_update
3086            .extend(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
3087        self.selected_entry.invalidate();
3088        self.update_fs_entries(new_active_editor, None, window, cx);
3089    }
3090
3091    fn clear_previous(&mut self, window: &mut Window, cx: &mut App) {
3092        self.fs_entries_update_task = Task::ready(());
3093        self.outline_fetch_tasks.clear();
3094        self.cached_entries_update_task = Task::ready(());
3095        self.reveal_selection_task = Task::ready(Ok(()));
3096        self.filter_editor
3097            .update(cx, |editor, cx| editor.clear(window, cx));
3098        self.collapsed_entries.clear();
3099        self.unfolded_dirs.clear();
3100        self.active_item = None;
3101        self.fs_entries.clear();
3102        self.fs_entries_depth.clear();
3103        self.fs_children_count.clear();
3104        self.excerpts.clear();
3105        self.cached_entries = Vec::new();
3106        self.selected_entry = SelectedEntry::None;
3107        self.pinned = false;
3108        self.mode = ItemsDisplayMode::Outline;
3109        self.pending_default_expansion_depth = None;
3110    }
3111
3112    fn location_for_editor_selection(
3113        &self,
3114        editor: &Entity<Editor>,
3115        window: &mut Window,
3116        cx: &mut Context<Self>,
3117    ) -> Option<PanelEntry> {
3118        let selection = editor.update(cx, |editor, cx| {
3119            editor.selections.newest::<language::Point>(cx).head()
3120        });
3121        let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
3122        let multi_buffer = editor.read(cx).buffer();
3123        let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
3124        let (excerpt_id, buffer, _) = editor
3125            .read(cx)
3126            .buffer()
3127            .read(cx)
3128            .excerpt_containing(selection, cx)?;
3129        let buffer_id = buffer.read(cx).remote_id();
3130
3131        if editor.read(cx).is_buffer_folded(buffer_id, cx) {
3132            return self
3133                .fs_entries
3134                .iter()
3135                .find(|fs_entry| match fs_entry {
3136                    FsEntry::Directory(..) => false,
3137                    FsEntry::File(FsEntryFile {
3138                        buffer_id: other_buffer_id,
3139                        ..
3140                    })
3141                    | FsEntry::ExternalFile(FsEntryExternalFile {
3142                        buffer_id: other_buffer_id,
3143                        ..
3144                    }) => buffer_id == *other_buffer_id,
3145                })
3146                .cloned()
3147                .map(PanelEntry::Fs);
3148        }
3149
3150        let selection_display_point = selection.to_display_point(&editor_snapshot);
3151
3152        match &self.mode {
3153            ItemsDisplayMode::Search(search_state) => search_state
3154                .matches
3155                .iter()
3156                .rev()
3157                .min_by_key(|&(match_range, _)| {
3158                    let match_display_range =
3159                        match_range.clone().to_display_points(&editor_snapshot);
3160                    let start_distance = if selection_display_point < match_display_range.start {
3161                        match_display_range.start - selection_display_point
3162                    } else {
3163                        selection_display_point - match_display_range.start
3164                    };
3165                    let end_distance = if selection_display_point < match_display_range.end {
3166                        match_display_range.end - selection_display_point
3167                    } else {
3168                        selection_display_point - match_display_range.end
3169                    };
3170                    start_distance + end_distance
3171                })
3172                .and_then(|(closest_range, _)| {
3173                    self.cached_entries.iter().find_map(|cached_entry| {
3174                        if let PanelEntry::Search(SearchEntry { match_range, .. }) =
3175                            &cached_entry.entry
3176                        {
3177                            if match_range == closest_range {
3178                                Some(cached_entry.entry.clone())
3179                            } else {
3180                                None
3181                            }
3182                        } else {
3183                            None
3184                        }
3185                    })
3186                }),
3187            ItemsDisplayMode::Outline => self.outline_location(
3188                buffer_id,
3189                excerpt_id,
3190                multi_buffer_snapshot,
3191                editor_snapshot,
3192                selection_display_point,
3193            ),
3194        }
3195    }
3196
3197    fn outline_location(
3198        &self,
3199        buffer_id: BufferId,
3200        excerpt_id: ExcerptId,
3201        multi_buffer_snapshot: editor::MultiBufferSnapshot,
3202        editor_snapshot: editor::EditorSnapshot,
3203        selection_display_point: DisplayPoint,
3204    ) -> Option<PanelEntry> {
3205        let excerpt_outlines = self
3206            .excerpts
3207            .get(&buffer_id)
3208            .and_then(|excerpts| excerpts.get(&excerpt_id))
3209            .into_iter()
3210            .flat_map(|excerpt| excerpt.iter_outlines())
3211            .flat_map(|outline| {
3212                let start = multi_buffer_snapshot
3213                    .anchor_in_excerpt(excerpt_id, outline.range.start)?
3214                    .to_display_point(&editor_snapshot);
3215                let end = multi_buffer_snapshot
3216                    .anchor_in_excerpt(excerpt_id, outline.range.end)?
3217                    .to_display_point(&editor_snapshot);
3218                Some((start..end, outline))
3219            })
3220            .collect::<Vec<_>>();
3221
3222        let mut matching_outline_indices = Vec::new();
3223        let mut children = HashMap::default();
3224        let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
3225
3226        for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
3227            if outline_range
3228                .to_inclusive()
3229                .contains(&selection_display_point)
3230            {
3231                matching_outline_indices.push(i);
3232            } else if (outline_range.start.row()..outline_range.end.row())
3233                .to_inclusive()
3234                .contains(&selection_display_point.row())
3235            {
3236                matching_outline_indices.push(i);
3237            }
3238
3239            while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
3240                if parent_outline.depth >= outline.depth
3241                    || !parent_range.contains(&outline_range.start)
3242                {
3243                    parents_stack.pop();
3244                } else {
3245                    break;
3246                }
3247            }
3248            if let Some((_, _, parent_index)) = parents_stack.last_mut() {
3249                children
3250                    .entry(*parent_index)
3251                    .or_insert_with(Vec::new)
3252                    .push(i);
3253            }
3254            parents_stack.push((outline_range, outline, i));
3255        }
3256
3257        let outline_item = matching_outline_indices
3258            .into_iter()
3259            .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
3260            .filter(|(i, _)| {
3261                children
3262                    .get(i)
3263                    .map(|children| {
3264                        children.iter().all(|child_index| {
3265                            excerpt_outlines
3266                                .get(*child_index)
3267                                .map(|(child_range, _)| child_range.start > selection_display_point)
3268                                .unwrap_or(false)
3269                        })
3270                    })
3271                    .unwrap_or(true)
3272            })
3273            .min_by_key(|(_, (outline_range, outline))| {
3274                let distance_from_start = if outline_range.start > selection_display_point {
3275                    outline_range.start - selection_display_point
3276                } else {
3277                    selection_display_point - outline_range.start
3278                };
3279                let distance_from_end = if outline_range.end > selection_display_point {
3280                    outline_range.end - selection_display_point
3281                } else {
3282                    selection_display_point - outline_range.end
3283                };
3284
3285                (
3286                    cmp::Reverse(outline.depth),
3287                    distance_from_start + distance_from_end,
3288                )
3289            })
3290            .map(|(_, (_, outline))| *outline)
3291            .cloned();
3292
3293        let closest_container = match outline_item {
3294            Some(outline) => PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
3295                buffer_id,
3296                excerpt_id,
3297                outline,
3298            })),
3299            None => {
3300                self.cached_entries.iter().rev().find_map(|cached_entry| {
3301                    match &cached_entry.entry {
3302                        PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
3303                            if excerpt.buffer_id == buffer_id && excerpt.id == excerpt_id {
3304                                Some(cached_entry.entry.clone())
3305                            } else {
3306                                None
3307                            }
3308                        }
3309                        PanelEntry::Fs(
3310                            FsEntry::ExternalFile(FsEntryExternalFile {
3311                                buffer_id: file_buffer_id,
3312                                excerpts: file_excerpts,
3313                            })
3314                            | FsEntry::File(FsEntryFile {
3315                                buffer_id: file_buffer_id,
3316                                excerpts: file_excerpts,
3317                                ..
3318                            }),
3319                        ) => {
3320                            if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
3321                                Some(cached_entry.entry.clone())
3322                            } else {
3323                                None
3324                            }
3325                        }
3326                        _ => None,
3327                    }
3328                })?
3329            }
3330        };
3331        Some(closest_container)
3332    }
3333
3334    fn fetch_outdated_outlines(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3335        let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
3336        if excerpt_fetch_ranges.is_empty() {
3337            return;
3338        }
3339
3340        let syntax_theme = cx.theme().syntax().clone();
3341        let first_update = Arc::new(AtomicBool::new(true));
3342        for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
3343            for (excerpt_id, excerpt_range) in excerpt_ranges {
3344                let syntax_theme = syntax_theme.clone();
3345                let buffer_snapshot = buffer_snapshot.clone();
3346                let first_update = first_update.clone();
3347                self.outline_fetch_tasks.insert(
3348                    (buffer_id, excerpt_id),
3349                    cx.spawn_in(window, async move |outline_panel, cx| {
3350                        let buffer_language = buffer_snapshot.language().cloned();
3351                        let fetched_outlines = cx
3352                            .background_spawn(async move {
3353                                let mut outlines = buffer_snapshot
3354                                    .outline_items_containing(
3355                                        excerpt_range.context,
3356                                        false,
3357                                        Some(&syntax_theme),
3358                                    )
3359                                    .unwrap_or_default();
3360                                outlines.retain(|outline| {
3361                                    buffer_language.is_none()
3362                                        || buffer_language.as_ref()
3363                                            == buffer_snapshot.language_at(outline.range.start)
3364                                });
3365
3366                                let outlines_with_children = outlines
3367                                    .windows(2)
3368                                    .filter_map(|window| {
3369                                        let current = &window[0];
3370                                        let next = &window[1];
3371                                        if next.depth > current.depth {
3372                                            Some((current.range.clone(), current.depth))
3373                                        } else {
3374                                            None
3375                                        }
3376                                    })
3377                                    .collect::<HashSet<_>>();
3378
3379                                (outlines, outlines_with_children)
3380                            })
3381                            .await;
3382
3383                        let (fetched_outlines, outlines_with_children) = fetched_outlines;
3384
3385                        outline_panel
3386                            .update_in(cx, |outline_panel, window, cx| {
3387                                let pending_default_depth =
3388                                    outline_panel.pending_default_expansion_depth.take();
3389
3390                                let debounce =
3391                                    if first_update.fetch_and(false, atomic::Ordering::AcqRel) {
3392                                        None
3393                                    } else {
3394                                        Some(UPDATE_DEBOUNCE)
3395                                    };
3396
3397                                if let Some(excerpt) = outline_panel
3398                                    .excerpts
3399                                    .entry(buffer_id)
3400                                    .or_default()
3401                                    .get_mut(&excerpt_id)
3402                                {
3403                                    excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
3404
3405                                    if let Some(default_depth) = pending_default_depth
3406                                        && let ExcerptOutlines::Outlines(outlines) =
3407                                            &excerpt.outlines
3408                                    {
3409                                        outlines
3410                                            .iter()
3411                                            .filter(|outline| {
3412                                                (default_depth == 0
3413                                                    || outline.depth >= default_depth)
3414                                                    && outlines_with_children.contains(&(
3415                                                        outline.range.clone(),
3416                                                        outline.depth,
3417                                                    ))
3418                                            })
3419                                            .for_each(|outline| {
3420                                                outline_panel.collapsed_entries.insert(
3421                                                    CollapsedEntry::Outline(
3422                                                        buffer_id,
3423                                                        excerpt_id,
3424                                                        outline.range.clone(),
3425                                                    ),
3426                                                );
3427                                            });
3428                                    }
3429
3430                                    // Even if no outlines to check, we still need to update cached entries
3431                                    // to show the outline entries that were just fetched
3432                                    outline_panel.update_cached_entries(debounce, window, cx);
3433                                }
3434                            })
3435                            .ok();
3436                    }),
3437                );
3438            }
3439        }
3440    }
3441
3442    fn is_singleton_active(&self, cx: &App) -> bool {
3443        self.active_editor()
3444            .is_some_and(|active_editor| active_editor.read(cx).buffer().read(cx).is_singleton())
3445    }
3446
3447    fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
3448        self.outline_fetch_tasks.clear();
3449        let mut ids = ids.iter().collect::<HashSet<_>>();
3450        for excerpts in self.excerpts.values_mut() {
3451            ids.retain(|id| {
3452                if let Some(excerpt) = excerpts.get_mut(id) {
3453                    excerpt.invalidate_outlines();
3454                    false
3455                } else {
3456                    true
3457                }
3458            });
3459            if ids.is_empty() {
3460                break;
3461            }
3462        }
3463    }
3464
3465    fn excerpt_fetch_ranges(
3466        &self,
3467        cx: &App,
3468    ) -> HashMap<
3469        BufferId,
3470        (
3471            BufferSnapshot,
3472            HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
3473        ),
3474    > {
3475        self.fs_entries
3476            .iter()
3477            .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
3478                match fs_entry {
3479                    FsEntry::File(FsEntryFile {
3480                        buffer_id,
3481                        excerpts: file_excerpts,
3482                        ..
3483                    })
3484                    | FsEntry::ExternalFile(FsEntryExternalFile {
3485                        buffer_id,
3486                        excerpts: file_excerpts,
3487                    }) => {
3488                        let excerpts = self.excerpts.get(buffer_id);
3489                        for &file_excerpt in file_excerpts {
3490                            if let Some(excerpt) = excerpts
3491                                .and_then(|excerpts| excerpts.get(&file_excerpt))
3492                                .filter(|excerpt| excerpt.should_fetch_outlines())
3493                            {
3494                                match excerpts_to_fetch.entry(*buffer_id) {
3495                                    hash_map::Entry::Occupied(mut o) => {
3496                                        o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
3497                                    }
3498                                    hash_map::Entry::Vacant(v) => {
3499                                        if let Some(buffer_snapshot) =
3500                                            self.buffer_snapshot_for_id(*buffer_id, cx)
3501                                        {
3502                                            v.insert((buffer_snapshot, HashMap::default()))
3503                                                .1
3504                                                .insert(file_excerpt, excerpt.range.clone());
3505                                        }
3506                                    }
3507                                }
3508                            }
3509                        }
3510                    }
3511                    FsEntry::Directory(..) => {}
3512                }
3513                excerpts_to_fetch
3514            })
3515    }
3516
3517    fn buffer_snapshot_for_id(&self, buffer_id: BufferId, cx: &App) -> Option<BufferSnapshot> {
3518        let editor = self.active_editor()?;
3519        Some(
3520            editor
3521                .read(cx)
3522                .buffer()
3523                .read(cx)
3524                .buffer(buffer_id)?
3525                .read(cx)
3526                .snapshot(),
3527        )
3528    }
3529
3530    fn abs_path(&self, entry: &PanelEntry, cx: &App) -> Option<PathBuf> {
3531        match entry {
3532            PanelEntry::Fs(
3533                FsEntry::File(FsEntryFile { buffer_id, .. })
3534                | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }),
3535            ) => self
3536                .buffer_snapshot_for_id(*buffer_id, cx)
3537                .and_then(|buffer_snapshot| {
3538                    let file = File::from_dyn(buffer_snapshot.file())?;
3539                    file.worktree.read(cx).absolutize(&file.path).ok()
3540                }),
3541            PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
3542                worktree_id, entry, ..
3543            })) => self
3544                .project
3545                .read(cx)
3546                .worktree_for_id(*worktree_id, cx)?
3547                .read(cx)
3548                .absolutize(&entry.path)
3549                .ok(),
3550            PanelEntry::FoldedDirs(FoldedDirsEntry {
3551                worktree_id,
3552                entries: dirs,
3553                ..
3554            }) => dirs.last().and_then(|entry| {
3555                self.project
3556                    .read(cx)
3557                    .worktree_for_id(*worktree_id, cx)
3558                    .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
3559            }),
3560            PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
3561        }
3562    }
3563
3564    fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option<Arc<Path>> {
3565        match entry {
3566            FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
3567                let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
3568                Some(buffer_snapshot.file()?.path().clone())
3569            }
3570            FsEntry::Directory(FsEntryDirectory { entry, .. }) => Some(entry.path.clone()),
3571            FsEntry::File(FsEntryFile { entry, .. }) => Some(entry.path.clone()),
3572        }
3573    }
3574
3575    fn update_cached_entries(
3576        &mut self,
3577        debounce: Option<Duration>,
3578        window: &mut Window,
3579        cx: &mut Context<OutlinePanel>,
3580    ) {
3581        if !self.active {
3582            return;
3583        }
3584
3585        let is_singleton = self.is_singleton_active(cx);
3586        let query = self.query(cx);
3587        self.updating_cached_entries = true;
3588        self.cached_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
3589            if let Some(debounce) = debounce {
3590                cx.background_executor().timer(debounce).await;
3591            }
3592            let Some(new_cached_entries) = outline_panel
3593                .update_in(cx, |outline_panel, window, cx| {
3594                    outline_panel.generate_cached_entries(is_singleton, query, window, cx)
3595                })
3596                .ok()
3597            else {
3598                return;
3599            };
3600            let (new_cached_entries, max_width_item_index) = new_cached_entries.await;
3601            outline_panel
3602                .update_in(cx, |outline_panel, window, cx| {
3603                    outline_panel.cached_entries = new_cached_entries;
3604                    outline_panel.max_width_item_index = max_width_item_index;
3605                    if (outline_panel.selected_entry.is_invalidated()
3606                        || matches!(outline_panel.selected_entry, SelectedEntry::None))
3607                        && let Some(new_selected_entry) =
3608                            outline_panel.active_editor().and_then(|active_editor| {
3609                                outline_panel.location_for_editor_selection(
3610                                    &active_editor,
3611                                    window,
3612                                    cx,
3613                                )
3614                            })
3615                    {
3616                        outline_panel.select_entry(new_selected_entry, false, window, cx);
3617                    }
3618
3619                    outline_panel.autoscroll(cx);
3620                    outline_panel.updating_cached_entries = false;
3621                    cx.notify();
3622                })
3623                .ok();
3624        });
3625    }
3626
3627    fn generate_cached_entries(
3628        &self,
3629        is_singleton: bool,
3630        query: Option<String>,
3631        window: &mut Window,
3632        cx: &mut Context<Self>,
3633    ) -> Task<(Vec<CachedEntry>, Option<usize>)> {
3634        let project = self.project.clone();
3635        let Some(active_editor) = self.active_editor() else {
3636            return Task::ready((Vec::new(), None));
3637        };
3638        cx.spawn_in(window, async move |outline_panel, cx| {
3639            let mut generation_state = GenerationState::default();
3640
3641            let Ok(()) = outline_panel.update(cx, |outline_panel, cx| {
3642                let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
3643                let mut folded_dirs_entry = None::<(usize, FoldedDirsEntry)>;
3644                let track_matches = query.is_some();
3645
3646                #[derive(Debug)]
3647                struct ParentStats {
3648                    path: Arc<Path>,
3649                    folded: bool,
3650                    expanded: bool,
3651                    depth: usize,
3652                }
3653                let mut parent_dirs = Vec::<ParentStats>::new();
3654                for entry in outline_panel.fs_entries.clone() {
3655                    let is_expanded = outline_panel.is_expanded(&entry);
3656                    let (depth, should_add) = match &entry {
3657                        FsEntry::Directory(directory_entry) => {
3658                            let mut should_add = true;
3659                            let is_root = project
3660                                .read(cx)
3661                                .worktree_for_id(directory_entry.worktree_id, cx)
3662                                .is_some_and(|worktree| {
3663                                    worktree.read(cx).root_entry() == Some(&directory_entry.entry)
3664                                });
3665                            let folded = auto_fold_dirs
3666                                && !is_root
3667                                && outline_panel
3668                                    .unfolded_dirs
3669                                    .get(&directory_entry.worktree_id)
3670                                    .is_none_or(|unfolded_dirs| {
3671                                        !unfolded_dirs.contains(&directory_entry.entry.id)
3672                                    });
3673                            let fs_depth = outline_panel
3674                                .fs_entries_depth
3675                                .get(&(directory_entry.worktree_id, directory_entry.entry.id))
3676                                .copied()
3677                                .unwrap_or(0);
3678                            while let Some(parent) = parent_dirs.last() {
3679                                if !is_root && directory_entry.entry.path.starts_with(&parent.path)
3680                                {
3681                                    break;
3682                                }
3683                                parent_dirs.pop();
3684                            }
3685                            let auto_fold = match parent_dirs.last() {
3686                                Some(parent) => {
3687                                    parent.folded
3688                                        && Some(parent.path.as_ref())
3689                                            == directory_entry.entry.path.parent()
3690                                        && outline_panel
3691                                            .fs_children_count
3692                                            .get(&directory_entry.worktree_id)
3693                                            .and_then(|entries| {
3694                                                entries.get(&directory_entry.entry.path)
3695                                            })
3696                                            .copied()
3697                                            .unwrap_or_default()
3698                                            .may_be_fold_part()
3699                                }
3700                                None => false,
3701                            };
3702                            let folded = folded || auto_fold;
3703                            let (depth, parent_expanded, parent_folded) = match parent_dirs.last() {
3704                                Some(parent) => {
3705                                    let parent_folded = parent.folded;
3706                                    let parent_expanded = parent.expanded;
3707                                    let new_depth = if parent_folded {
3708                                        parent.depth
3709                                    } else {
3710                                        parent.depth + 1
3711                                    };
3712                                    parent_dirs.push(ParentStats {
3713                                        path: directory_entry.entry.path.clone(),
3714                                        folded,
3715                                        expanded: parent_expanded && is_expanded,
3716                                        depth: new_depth,
3717                                    });
3718                                    (new_depth, parent_expanded, parent_folded)
3719                                }
3720                                None => {
3721                                    parent_dirs.push(ParentStats {
3722                                        path: directory_entry.entry.path.clone(),
3723                                        folded,
3724                                        expanded: is_expanded,
3725                                        depth: fs_depth,
3726                                    });
3727                                    (fs_depth, true, false)
3728                                }
3729                            };
3730
3731                            if let Some((folded_depth, mut folded_dirs)) = folded_dirs_entry.take()
3732                            {
3733                                if folded
3734                                    && directory_entry.worktree_id == folded_dirs.worktree_id
3735                                    && directory_entry.entry.path.parent()
3736                                        == folded_dirs
3737                                            .entries
3738                                            .last()
3739                                            .map(|entry| entry.path.as_ref())
3740                                {
3741                                    folded_dirs.entries.push(directory_entry.entry.clone());
3742                                    folded_dirs_entry = Some((folded_depth, folded_dirs))
3743                                } else {
3744                                    if !is_singleton {
3745                                        let start_of_collapsed_dir_sequence = !parent_expanded
3746                                            && parent_dirs
3747                                                .iter()
3748                                                .rev()
3749                                                .nth(folded_dirs.entries.len() + 1)
3750                                                .is_none_or(|parent| parent.expanded);
3751                                        if start_of_collapsed_dir_sequence
3752                                            || parent_expanded
3753                                            || query.is_some()
3754                                        {
3755                                            if parent_folded {
3756                                                folded_dirs
3757                                                    .entries
3758                                                    .push(directory_entry.entry.clone());
3759                                                should_add = false;
3760                                            }
3761                                            let new_folded_dirs =
3762                                                PanelEntry::FoldedDirs(folded_dirs.clone());
3763                                            outline_panel.push_entry(
3764                                                &mut generation_state,
3765                                                track_matches,
3766                                                new_folded_dirs,
3767                                                folded_depth,
3768                                                cx,
3769                                            );
3770                                        }
3771                                    }
3772
3773                                    folded_dirs_entry = if parent_folded {
3774                                        None
3775                                    } else {
3776                                        Some((
3777                                            depth,
3778                                            FoldedDirsEntry {
3779                                                worktree_id: directory_entry.worktree_id,
3780                                                entries: vec![directory_entry.entry.clone()],
3781                                            },
3782                                        ))
3783                                    };
3784                                }
3785                            } else if folded {
3786                                folded_dirs_entry = Some((
3787                                    depth,
3788                                    FoldedDirsEntry {
3789                                        worktree_id: directory_entry.worktree_id,
3790                                        entries: vec![directory_entry.entry.clone()],
3791                                    },
3792                                ));
3793                            }
3794
3795                            let should_add =
3796                                should_add && parent_expanded && folded_dirs_entry.is_none();
3797                            (depth, should_add)
3798                        }
3799                        FsEntry::ExternalFile(..) => {
3800                            if let Some((folded_depth, folded_dir)) = folded_dirs_entry.take() {
3801                                let parent_expanded = parent_dirs
3802                                    .iter()
3803                                    .rev()
3804                                    .find(|parent| {
3805                                        folded_dir
3806                                            .entries
3807                                            .iter()
3808                                            .all(|entry| entry.path != parent.path)
3809                                    })
3810                                    .is_none_or(|parent| parent.expanded);
3811                                if !is_singleton && (parent_expanded || query.is_some()) {
3812                                    outline_panel.push_entry(
3813                                        &mut generation_state,
3814                                        track_matches,
3815                                        PanelEntry::FoldedDirs(folded_dir),
3816                                        folded_depth,
3817                                        cx,
3818                                    );
3819                                }
3820                            }
3821                            parent_dirs.clear();
3822                            (0, true)
3823                        }
3824                        FsEntry::File(file) => {
3825                            if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3826                                let parent_expanded = parent_dirs
3827                                    .iter()
3828                                    .rev()
3829                                    .find(|parent| {
3830                                        folded_dirs
3831                                            .entries
3832                                            .iter()
3833                                            .all(|entry| entry.path != parent.path)
3834                                    })
3835                                    .is_none_or(|parent| parent.expanded);
3836                                if !is_singleton && (parent_expanded || query.is_some()) {
3837                                    outline_panel.push_entry(
3838                                        &mut generation_state,
3839                                        track_matches,
3840                                        PanelEntry::FoldedDirs(folded_dirs),
3841                                        folded_depth,
3842                                        cx,
3843                                    );
3844                                }
3845                            }
3846
3847                            let fs_depth = outline_panel
3848                                .fs_entries_depth
3849                                .get(&(file.worktree_id, file.entry.id))
3850                                .copied()
3851                                .unwrap_or(0);
3852                            while let Some(parent) = parent_dirs.last() {
3853                                if file.entry.path.starts_with(&parent.path) {
3854                                    break;
3855                                }
3856                                parent_dirs.pop();
3857                            }
3858                            match parent_dirs.last() {
3859                                Some(parent) => {
3860                                    let new_depth = parent.depth + 1;
3861                                    (new_depth, parent.expanded)
3862                                }
3863                                None => (fs_depth, true),
3864                            }
3865                        }
3866                    };
3867
3868                    if !is_singleton
3869                        && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
3870                    {
3871                        outline_panel.push_entry(
3872                            &mut generation_state,
3873                            track_matches,
3874                            PanelEntry::Fs(entry.clone()),
3875                            depth,
3876                            cx,
3877                        );
3878                    }
3879
3880                    match outline_panel.mode {
3881                        ItemsDisplayMode::Search(_) => {
3882                            if is_singleton || query.is_some() || (should_add && is_expanded) {
3883                                outline_panel.add_search_entries(
3884                                    &mut generation_state,
3885                                    &active_editor,
3886                                    entry.clone(),
3887                                    depth,
3888                                    query.clone(),
3889                                    is_singleton,
3890                                    cx,
3891                                );
3892                            }
3893                        }
3894                        ItemsDisplayMode::Outline => {
3895                            let excerpts_to_consider =
3896                                if is_singleton || query.is_some() || (should_add && is_expanded) {
3897                                    match &entry {
3898                                        FsEntry::File(FsEntryFile {
3899                                            buffer_id,
3900                                            excerpts,
3901                                            ..
3902                                        })
3903                                        | FsEntry::ExternalFile(FsEntryExternalFile {
3904                                            buffer_id,
3905                                            excerpts,
3906                                            ..
3907                                        }) => Some((*buffer_id, excerpts)),
3908                                        _ => None,
3909                                    }
3910                                } else {
3911                                    None
3912                                };
3913                            if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider
3914                                && !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
3915                            {
3916                                outline_panel.add_excerpt_entries(
3917                                    &mut generation_state,
3918                                    buffer_id,
3919                                    entry_excerpts,
3920                                    depth,
3921                                    track_matches,
3922                                    is_singleton,
3923                                    query.as_deref(),
3924                                    cx,
3925                                );
3926                            }
3927                        }
3928                    }
3929
3930                    if is_singleton
3931                        && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
3932                        && !generation_state.entries.iter().any(|item| {
3933                            matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
3934                        })
3935                    {
3936                        outline_panel.push_entry(
3937                            &mut generation_state,
3938                            track_matches,
3939                            PanelEntry::Fs(entry.clone()),
3940                            0,
3941                            cx,
3942                        );
3943                    }
3944                }
3945
3946                if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3947                    let parent_expanded = parent_dirs
3948                        .iter()
3949                        .rev()
3950                        .find(|parent| {
3951                            folded_dirs
3952                                .entries
3953                                .iter()
3954                                .all(|entry| entry.path != parent.path)
3955                        })
3956                        .is_none_or(|parent| parent.expanded);
3957                    if parent_expanded || query.is_some() {
3958                        outline_panel.push_entry(
3959                            &mut generation_state,
3960                            track_matches,
3961                            PanelEntry::FoldedDirs(folded_dirs),
3962                            folded_depth,
3963                            cx,
3964                        );
3965                    }
3966                }
3967            }) else {
3968                return (Vec::new(), None);
3969            };
3970
3971            let Some(query) = query else {
3972                return (
3973                    generation_state.entries,
3974                    generation_state
3975                        .max_width_estimate_and_index
3976                        .map(|(_, index)| index),
3977                );
3978            };
3979
3980            let mut matched_ids = match_strings(
3981                &generation_state.match_candidates,
3982                &query,
3983                true,
3984                true,
3985                usize::MAX,
3986                &AtomicBool::default(),
3987                cx.background_executor().clone(),
3988            )
3989            .await
3990            .into_iter()
3991            .map(|string_match| (string_match.candidate_id, string_match))
3992            .collect::<HashMap<_, _>>();
3993
3994            let mut id = 0;
3995            generation_state.entries.retain_mut(|cached_entry| {
3996                let retain = match matched_ids.remove(&id) {
3997                    Some(string_match) => {
3998                        cached_entry.string_match = Some(string_match);
3999                        true
4000                    }
4001                    None => false,
4002                };
4003                id += 1;
4004                retain
4005            });
4006
4007            (
4008                generation_state.entries,
4009                generation_state
4010                    .max_width_estimate_and_index
4011                    .map(|(_, index)| index),
4012            )
4013        })
4014    }
4015
4016    fn push_entry(
4017        &self,
4018        state: &mut GenerationState,
4019        track_matches: bool,
4020        entry: PanelEntry,
4021        depth: usize,
4022        cx: &mut App,
4023    ) {
4024        let entry = if let PanelEntry::FoldedDirs(folded_dirs_entry) = &entry {
4025            match folded_dirs_entry.entries.len() {
4026                0 => {
4027                    debug_panic!("Empty folded dirs receiver");
4028                    return;
4029                }
4030                1 => PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
4031                    worktree_id: folded_dirs_entry.worktree_id,
4032                    entry: folded_dirs_entry.entries[0].clone(),
4033                })),
4034                _ => entry,
4035            }
4036        } else {
4037            entry
4038        };
4039
4040        if track_matches {
4041            let id = state.entries.len();
4042            match &entry {
4043                PanelEntry::Fs(fs_entry) => {
4044                    if let Some(file_name) =
4045                        self.relative_path(fs_entry, cx).as_deref().map(file_name)
4046                    {
4047                        state
4048                            .match_candidates
4049                            .push(StringMatchCandidate::new(id, &file_name));
4050                    }
4051                }
4052                PanelEntry::FoldedDirs(folded_dir_entry) => {
4053                    let dir_names = self.dir_names_string(
4054                        &folded_dir_entry.entries,
4055                        folded_dir_entry.worktree_id,
4056                        cx,
4057                    );
4058                    {
4059                        state
4060                            .match_candidates
4061                            .push(StringMatchCandidate::new(id, &dir_names));
4062                    }
4063                }
4064                PanelEntry::Outline(OutlineEntry::Outline(outline_entry)) => state
4065                    .match_candidates
4066                    .push(StringMatchCandidate::new(id, &outline_entry.outline.text)),
4067                PanelEntry::Outline(OutlineEntry::Excerpt(_)) => {}
4068                PanelEntry::Search(new_search_entry) => {
4069                    if let Some(search_data) = new_search_entry.render_data.get() {
4070                        state
4071                            .match_candidates
4072                            .push(StringMatchCandidate::new(id, &search_data.context_text));
4073                    }
4074                }
4075            }
4076        }
4077
4078        let width_estimate = self.width_estimate(depth, &entry, cx);
4079        if Some(width_estimate)
4080            > state
4081                .max_width_estimate_and_index
4082                .map(|(estimate, _)| estimate)
4083        {
4084            state.max_width_estimate_and_index = Some((width_estimate, state.entries.len()));
4085        }
4086        state.entries.push(CachedEntry {
4087            depth,
4088            entry,
4089            string_match: None,
4090        });
4091    }
4092
4093    fn dir_names_string(&self, entries: &[GitEntry], worktree_id: WorktreeId, cx: &App) -> String {
4094        let dir_names_segment = entries
4095            .iter()
4096            .map(|entry| self.entry_name(&worktree_id, entry, cx))
4097            .collect::<PathBuf>();
4098        dir_names_segment.to_string_lossy().to_string()
4099    }
4100
4101    fn query(&self, cx: &App) -> Option<String> {
4102        let query = self.filter_editor.read(cx).text(cx);
4103        if query.trim().is_empty() {
4104            None
4105        } else {
4106            Some(query)
4107        }
4108    }
4109
4110    fn is_expanded(&self, entry: &FsEntry) -> bool {
4111        let entry_to_check = match entry {
4112            FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
4113                CollapsedEntry::ExternalFile(*buffer_id)
4114            }
4115            FsEntry::File(FsEntryFile {
4116                worktree_id,
4117                buffer_id,
4118                ..
4119            }) => CollapsedEntry::File(*worktree_id, *buffer_id),
4120            FsEntry::Directory(FsEntryDirectory {
4121                worktree_id, entry, ..
4122            }) => CollapsedEntry::Dir(*worktree_id, entry.id),
4123        };
4124        !self.collapsed_entries.contains(&entry_to_check)
4125    }
4126
4127    fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) -> bool {
4128        if !self.active {
4129            return false;
4130        }
4131
4132        let mut update_cached_items = false;
4133        update_cached_items |= self.update_search_matches(window, cx);
4134        self.fetch_outdated_outlines(window, cx);
4135        if update_cached_items {
4136            self.selected_entry.invalidate();
4137        }
4138        update_cached_items
4139    }
4140
4141    fn update_search_matches(
4142        &mut self,
4143        window: &mut Window,
4144        cx: &mut Context<OutlinePanel>,
4145    ) -> bool {
4146        if !self.active {
4147            return false;
4148        }
4149
4150        let project_search = self
4151            .active_item()
4152            .and_then(|item| item.downcast::<ProjectSearchView>());
4153        let project_search_matches = project_search
4154            .as_ref()
4155            .map(|project_search| project_search.read(cx).get_matches(cx))
4156            .unwrap_or_default();
4157
4158        let buffer_search = self
4159            .active_item()
4160            .as_deref()
4161            .and_then(|active_item| {
4162                self.workspace
4163                    .upgrade()
4164                    .and_then(|workspace| workspace.read(cx).pane_for(active_item))
4165            })
4166            .and_then(|pane| {
4167                pane.read(cx)
4168                    .toolbar()
4169                    .read(cx)
4170                    .item_of_type::<BufferSearchBar>()
4171            });
4172        let buffer_search_matches = self
4173            .active_editor()
4174            .map(|active_editor| {
4175                active_editor.update(cx, |editor, cx| editor.get_matches(window, cx))
4176            })
4177            .unwrap_or_default();
4178
4179        let mut update_cached_entries = false;
4180        if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
4181            if matches!(self.mode, ItemsDisplayMode::Search(_)) {
4182                self.mode = ItemsDisplayMode::Outline;
4183                update_cached_entries = true;
4184            }
4185        } else {
4186            let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() {
4187                (
4188                    SearchKind::Project,
4189                    project_search_matches,
4190                    project_search
4191                        .map(|project_search| project_search.read(cx).search_query_text(cx))
4192                        .unwrap_or_default(),
4193                )
4194            } else {
4195                (
4196                    SearchKind::Buffer,
4197                    buffer_search_matches,
4198                    buffer_search
4199                        .map(|buffer_search| buffer_search.read(cx).query(cx))
4200                        .unwrap_or_default(),
4201                )
4202            };
4203
4204            let mut previous_matches = HashMap::default();
4205            update_cached_entries = match &mut self.mode {
4206                ItemsDisplayMode::Search(current_search_state) => {
4207                    let update = current_search_state.query != new_search_query
4208                        || current_search_state.kind != kind
4209                        || current_search_state.matches.is_empty()
4210                        || current_search_state.matches.iter().enumerate().any(
4211                            |(i, (match_range, _))| new_search_matches.get(i) != Some(match_range),
4212                        );
4213                    if current_search_state.kind == kind {
4214                        previous_matches.extend(current_search_state.matches.drain(..));
4215                    }
4216                    update
4217                }
4218                ItemsDisplayMode::Outline => true,
4219            };
4220            self.mode = ItemsDisplayMode::Search(SearchState::new(
4221                kind,
4222                new_search_query,
4223                previous_matches,
4224                new_search_matches,
4225                cx.theme().syntax().clone(),
4226                window,
4227                cx,
4228            ));
4229        }
4230        update_cached_entries
4231    }
4232
4233    fn add_excerpt_entries(
4234        &mut self,
4235        state: &mut GenerationState,
4236        buffer_id: BufferId,
4237        entries_to_add: &[ExcerptId],
4238        parent_depth: usize,
4239        track_matches: bool,
4240        is_singleton: bool,
4241        query: Option<&str>,
4242        cx: &mut Context<Self>,
4243    ) {
4244        if let Some(excerpts) = self.excerpts.get(&buffer_id) {
4245            let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx);
4246
4247            for &excerpt_id in entries_to_add {
4248                let Some(excerpt) = excerpts.get(&excerpt_id) else {
4249                    continue;
4250                };
4251                let excerpt_depth = parent_depth + 1;
4252                self.push_entry(
4253                    state,
4254                    track_matches,
4255                    PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt {
4256                        buffer_id,
4257                        id: excerpt_id,
4258                        range: excerpt.range.clone(),
4259                    })),
4260                    excerpt_depth,
4261                    cx,
4262                );
4263
4264                let mut outline_base_depth = excerpt_depth + 1;
4265                if is_singleton {
4266                    outline_base_depth = 0;
4267                    state.clear();
4268                } else if query.is_none()
4269                    && self
4270                        .collapsed_entries
4271                        .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
4272                {
4273                    continue;
4274                }
4275
4276                let mut last_depth_at_level: Vec<Option<Range<Anchor>>> = vec![None; 10];
4277
4278                let all_outlines: Vec<_> = excerpt.iter_outlines().collect();
4279
4280                let mut outline_has_children = HashMap::default();
4281                let mut visible_outlines = Vec::new();
4282                let mut collapsed_state: Option<(usize, Range<Anchor>)> = None;
4283
4284                for (i, &outline) in all_outlines.iter().enumerate() {
4285                    let has_children = all_outlines
4286                        .get(i + 1)
4287                        .map(|next| next.depth > outline.depth)
4288                        .unwrap_or(false);
4289
4290                    outline_has_children
4291                        .insert((outline.range.clone(), outline.depth), has_children);
4292
4293                    let mut should_include = true;
4294
4295                    if let Some((collapsed_depth, collapsed_range)) = &collapsed_state {
4296                        if outline.depth <= *collapsed_depth {
4297                            collapsed_state = None;
4298                        } else if let Some(buffer_snapshot) = buffer_snapshot.as_ref() {
4299                            let outline_start = outline.range.start;
4300                            if outline_start
4301                                .cmp(&collapsed_range.start, buffer_snapshot)
4302                                .is_ge()
4303                                && outline_start
4304                                    .cmp(&collapsed_range.end, buffer_snapshot)
4305                                    .is_lt()
4306                            {
4307                                should_include = false; // Skip - inside collapsed range
4308                            } else {
4309                                collapsed_state = None;
4310                            }
4311                        }
4312                    }
4313
4314                    // Check if this outline itself is collapsed
4315                    if should_include
4316                        && self.collapsed_entries.contains(&CollapsedEntry::Outline(
4317                            buffer_id,
4318                            excerpt_id,
4319                            outline.range.clone(),
4320                        ))
4321                    {
4322                        collapsed_state = Some((outline.depth, outline.range.clone()));
4323                    }
4324
4325                    if should_include {
4326                        visible_outlines.push(outline);
4327                    }
4328                }
4329
4330                self.outline_children_cache
4331                    .entry(buffer_id)
4332                    .or_default()
4333                    .extend(outline_has_children);
4334
4335                for outline in visible_outlines {
4336                    let outline_entry = OutlineEntryOutline {
4337                        buffer_id,
4338                        excerpt_id,
4339                        outline: outline.clone(),
4340                    };
4341
4342                    if outline.depth < last_depth_at_level.len() {
4343                        last_depth_at_level[outline.depth] = Some(outline.range.clone());
4344                        // Clear deeper levels when we go back to a shallower depth
4345                        for d in (outline.depth + 1)..last_depth_at_level.len() {
4346                            last_depth_at_level[d] = None;
4347                        }
4348                    }
4349
4350                    self.push_entry(
4351                        state,
4352                        track_matches,
4353                        PanelEntry::Outline(OutlineEntry::Outline(outline_entry)),
4354                        outline_base_depth + outline.depth,
4355                        cx,
4356                    );
4357                }
4358            }
4359        }
4360    }
4361
4362    fn add_search_entries(
4363        &mut self,
4364        state: &mut GenerationState,
4365        active_editor: &Entity<Editor>,
4366        parent_entry: FsEntry,
4367        parent_depth: usize,
4368        filter_query: Option<String>,
4369        is_singleton: bool,
4370        cx: &mut Context<Self>,
4371    ) {
4372        let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
4373            return;
4374        };
4375
4376        let kind = search_state.kind;
4377        let related_excerpts = match &parent_entry {
4378            FsEntry::Directory(_) => return,
4379            FsEntry::ExternalFile(external) => &external.excerpts,
4380            FsEntry::File(file) => &file.excerpts,
4381        }
4382        .iter()
4383        .copied()
4384        .collect::<HashSet<_>>();
4385
4386        let depth = if is_singleton { 0 } else { parent_depth + 1 };
4387        let new_search_matches = search_state
4388            .matches
4389            .iter()
4390            .filter(|(match_range, _)| {
4391                related_excerpts.contains(&match_range.start.excerpt_id)
4392                    || related_excerpts.contains(&match_range.end.excerpt_id)
4393            })
4394            .filter(|(match_range, _)| {
4395                let editor = active_editor.read(cx);
4396                let snapshot = editor.buffer().read(cx).snapshot(cx);
4397                if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.start)
4398                    && editor.is_buffer_folded(buffer_id, cx)
4399                {
4400                    return false;
4401                }
4402                if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.end)
4403                    && editor.is_buffer_folded(buffer_id, cx)
4404                {
4405                    return false;
4406                }
4407                true
4408            });
4409
4410        let new_search_entries = new_search_matches
4411            .map(|(match_range, search_data)| SearchEntry {
4412                match_range: match_range.clone(),
4413                kind,
4414                render_data: Arc::clone(search_data),
4415            })
4416            .collect::<Vec<_>>();
4417        for new_search_entry in new_search_entries {
4418            self.push_entry(
4419                state,
4420                filter_query.is_some(),
4421                PanelEntry::Search(new_search_entry),
4422                depth,
4423                cx,
4424            );
4425        }
4426    }
4427
4428    fn active_editor(&self) -> Option<Entity<Editor>> {
4429        self.active_item.as_ref()?.active_editor.upgrade()
4430    }
4431
4432    fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
4433        self.active_item.as_ref()?.item_handle.upgrade()
4434    }
4435
4436    fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool {
4437        self.active_item().is_none_or(|active_item| {
4438            !self.pinned && active_item.item_id() != new_active_item.item_id()
4439        })
4440    }
4441
4442    pub fn toggle_active_editor_pin(
4443        &mut self,
4444        _: &ToggleActiveEditorPin,
4445        window: &mut Window,
4446        cx: &mut Context<Self>,
4447    ) {
4448        self.pinned = !self.pinned;
4449        if !self.pinned
4450            && let Some((active_item, active_editor)) = self
4451                .workspace
4452                .upgrade()
4453                .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
4454            && self.should_replace_active_item(active_item.as_ref())
4455        {
4456            self.replace_active_editor(active_item, active_editor, window, cx);
4457        }
4458
4459        cx.notify();
4460    }
4461
4462    fn selected_entry(&self) -> Option<&PanelEntry> {
4463        match &self.selected_entry {
4464            SelectedEntry::Invalidated(entry) => entry.as_ref(),
4465            SelectedEntry::Valid(entry, _) => Some(entry),
4466            SelectedEntry::None => None,
4467        }
4468    }
4469
4470    fn select_entry(
4471        &mut self,
4472        entry: PanelEntry,
4473        focus: bool,
4474        window: &mut Window,
4475        cx: &mut Context<Self>,
4476    ) {
4477        if focus {
4478            self.focus_handle.focus(window);
4479        }
4480        let ix = self
4481            .cached_entries
4482            .iter()
4483            .enumerate()
4484            .find(|(_, cached_entry)| &cached_entry.entry == &entry)
4485            .map(|(i, _)| i)
4486            .unwrap_or_default();
4487
4488        self.selected_entry = SelectedEntry::Valid(entry, ix);
4489
4490        self.autoscroll(cx);
4491        cx.notify();
4492    }
4493
4494    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4495        if !Self::should_show_scrollbar(cx)
4496            || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4497        {
4498            return None;
4499        }
4500        Some(
4501            div()
4502                .occlude()
4503                .id("project-panel-vertical-scroll")
4504                .on_mouse_move(cx.listener(|_, _, _, cx| {
4505                    cx.notify();
4506                    cx.stop_propagation()
4507                }))
4508                .on_hover(|_, _, cx| {
4509                    cx.stop_propagation();
4510                })
4511                .on_any_mouse_down(|_, _, cx| {
4512                    cx.stop_propagation();
4513                })
4514                .on_mouse_up(
4515                    MouseButton::Left,
4516                    cx.listener(|outline_panel, _, window, cx| {
4517                        if !outline_panel.vertical_scrollbar_state.is_dragging()
4518                            && !outline_panel.focus_handle.contains_focused(window, cx)
4519                        {
4520                            outline_panel.hide_scrollbar(window, cx);
4521                            cx.notify();
4522                        }
4523
4524                        cx.stop_propagation();
4525                    }),
4526                )
4527                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4528                    cx.notify();
4529                }))
4530                .h_full()
4531                .absolute()
4532                .right_1()
4533                .top_1()
4534                .bottom_0()
4535                .w(px(12.))
4536                .cursor_default()
4537                .children(Scrollbar::vertical(self.vertical_scrollbar_state.clone())),
4538        )
4539    }
4540
4541    fn render_horizontal_scrollbar(
4542        &self,
4543        _: &mut Window,
4544        cx: &mut Context<Self>,
4545    ) -> Option<Stateful<Div>> {
4546        if !Self::should_show_scrollbar(cx)
4547            || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4548        {
4549            return None;
4550        }
4551        Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| {
4552            div()
4553                .occlude()
4554                .id("project-panel-horizontal-scroll")
4555                .on_mouse_move(cx.listener(|_, _, _, cx| {
4556                    cx.notify();
4557                    cx.stop_propagation()
4558                }))
4559                .on_hover(|_, _, cx| {
4560                    cx.stop_propagation();
4561                })
4562                .on_any_mouse_down(|_, _, cx| {
4563                    cx.stop_propagation();
4564                })
4565                .on_mouse_up(
4566                    MouseButton::Left,
4567                    cx.listener(|outline_panel, _, window, cx| {
4568                        if !outline_panel.horizontal_scrollbar_state.is_dragging()
4569                            && !outline_panel.focus_handle.contains_focused(window, cx)
4570                        {
4571                            outline_panel.hide_scrollbar(window, cx);
4572                            cx.notify();
4573                        }
4574
4575                        cx.stop_propagation();
4576                    }),
4577                )
4578                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4579                    cx.notify();
4580                }))
4581                .w_full()
4582                .absolute()
4583                .right_1()
4584                .left_1()
4585                .bottom_0()
4586                .h(px(12.))
4587                .cursor_default()
4588                .child(scrollbar)
4589        })
4590    }
4591
4592    fn should_show_scrollbar(cx: &App) -> bool {
4593        let show = OutlinePanelSettings::get_global(cx)
4594            .scrollbar
4595            .show
4596            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4597        match show {
4598            ShowScrollbar::Auto => true,
4599            ShowScrollbar::System => true,
4600            ShowScrollbar::Always => true,
4601            ShowScrollbar::Never => false,
4602        }
4603    }
4604
4605    fn should_autohide_scrollbar(cx: &App) -> bool {
4606        let show = OutlinePanelSettings::get_global(cx)
4607            .scrollbar
4608            .show
4609            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4610        match show {
4611            ShowScrollbar::Auto => true,
4612            ShowScrollbar::System => cx
4613                .try_global::<ScrollbarAutoHide>()
4614                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4615            ShowScrollbar::Always => false,
4616            ShowScrollbar::Never => true,
4617        }
4618    }
4619
4620    fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4621        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4622        if !Self::should_autohide_scrollbar(cx) {
4623            return;
4624        }
4625        self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
4626            cx.background_executor()
4627                .timer(SCROLLBAR_SHOW_INTERVAL)
4628                .await;
4629            panel
4630                .update(cx, |panel, cx| {
4631                    panel.show_scrollbar = false;
4632                    cx.notify();
4633                })
4634                .log_err();
4635        }))
4636    }
4637
4638    fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 {
4639        let item_text_chars = match entry {
4640            PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
4641                .buffer_snapshot_for_id(external.buffer_id, cx)
4642                .and_then(|snapshot| {
4643                    Some(snapshot.file()?.path().file_name()?.to_string_lossy().len())
4644                })
4645                .unwrap_or_default(),
4646            PanelEntry::Fs(FsEntry::Directory(directory)) => directory
4647                .entry
4648                .path
4649                .file_name()
4650                .map(|name| name.to_string_lossy().len())
4651                .unwrap_or_default(),
4652            PanelEntry::Fs(FsEntry::File(file)) => file
4653                .entry
4654                .path
4655                .file_name()
4656                .map(|name| name.to_string_lossy().len())
4657                .unwrap_or_default(),
4658            PanelEntry::FoldedDirs(folded_dirs) => {
4659                folded_dirs
4660                    .entries
4661                    .iter()
4662                    .map(|dir| {
4663                        dir.path
4664                            .file_name()
4665                            .map(|name| name.to_string_lossy().len())
4666                            .unwrap_or_default()
4667                    })
4668                    .sum::<usize>()
4669                    + folded_dirs.entries.len().saturating_sub(1) * MAIN_SEPARATOR_STR.len()
4670            }
4671            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
4672                .excerpt_label(excerpt.buffer_id, &excerpt.range, cx)
4673                .map(|label| label.len())
4674                .unwrap_or_default(),
4675            PanelEntry::Outline(OutlineEntry::Outline(entry)) => entry.outline.text.len(),
4676            PanelEntry::Search(search) => search
4677                .render_data
4678                .get()
4679                .map(|data| data.context_text.len())
4680                .unwrap_or_default(),
4681        };
4682
4683        (item_text_chars + depth) as u64
4684    }
4685
4686    fn render_main_contents(
4687        &mut self,
4688        query: Option<String>,
4689        show_indent_guides: bool,
4690        indent_size: f32,
4691        window: &mut Window,
4692        cx: &mut Context<Self>,
4693    ) -> Div {
4694        let contents = if self.cached_entries.is_empty() {
4695            let header = if self.updating_fs_entries || self.updating_cached_entries {
4696                None
4697            } else if query.is_some() {
4698                Some("No matches for query")
4699            } else {
4700                Some("No outlines available")
4701            };
4702
4703            v_flex()
4704                .flex_1()
4705                .justify_center()
4706                .size_full()
4707                .when_some(header, |panel, header| {
4708                    panel
4709                        .child(h_flex().justify_center().child(Label::new(header)))
4710                        .when_some(query.clone(), |panel, query| {
4711                            panel.child(h_flex().justify_center().child(Label::new(query)))
4712                        })
4713                        .child(
4714                            h_flex()
4715                                .pt(DynamicSpacing::Base04.rems(cx))
4716                                .justify_center()
4717                                .child({
4718                                    let keystroke =
4719                                        match self.position(window, cx) {
4720                                            DockPosition::Left => window
4721                                                .keystroke_text_for(&workspace::ToggleLeftDock),
4722                                            DockPosition::Bottom => window
4723                                                .keystroke_text_for(&workspace::ToggleBottomDock),
4724                                            DockPosition::Right => window
4725                                                .keystroke_text_for(&workspace::ToggleRightDock),
4726                                        };
4727                                    Label::new(format!("Toggle this panel with {keystroke}"))
4728                                }),
4729                        )
4730                })
4731        } else {
4732            let list_contents = {
4733                let items_len = self.cached_entries.len();
4734                let multi_buffer_snapshot = self
4735                    .active_editor()
4736                    .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
4737                uniform_list(
4738                    "entries",
4739                    items_len,
4740                    cx.processor(move |outline_panel, range: Range<usize>, window, cx| {
4741                        let entries = outline_panel.cached_entries.get(range);
4742                        entries
4743                            .map(|entries| entries.to_vec())
4744                            .unwrap_or_default()
4745                            .into_iter()
4746                            .filter_map(|cached_entry| match cached_entry.entry {
4747                                PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
4748                                    &entry,
4749                                    cached_entry.depth,
4750                                    cached_entry.string_match.as_ref(),
4751                                    window,
4752                                    cx,
4753                                )),
4754                                PanelEntry::FoldedDirs(folded_dirs_entry) => {
4755                                    Some(outline_panel.render_folded_dirs(
4756                                        &folded_dirs_entry,
4757                                        cached_entry.depth,
4758                                        cached_entry.string_match.as_ref(),
4759                                        window,
4760                                        cx,
4761                                    ))
4762                                }
4763                                PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
4764                                    outline_panel.render_excerpt(
4765                                        &excerpt,
4766                                        cached_entry.depth,
4767                                        window,
4768                                        cx,
4769                                    )
4770                                }
4771                                PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
4772                                    Some(outline_panel.render_outline(
4773                                        &entry,
4774                                        cached_entry.depth,
4775                                        cached_entry.string_match.as_ref(),
4776                                        window,
4777                                        cx,
4778                                    ))
4779                                }
4780                                PanelEntry::Search(SearchEntry {
4781                                    match_range,
4782                                    render_data,
4783                                    kind,
4784                                    ..
4785                                }) => outline_panel.render_search_match(
4786                                    multi_buffer_snapshot.as_ref(),
4787                                    &match_range,
4788                                    &render_data,
4789                                    kind,
4790                                    cached_entry.depth,
4791                                    cached_entry.string_match.as_ref(),
4792                                    window,
4793                                    cx,
4794                                ),
4795                            })
4796                            .collect()
4797                    }),
4798                )
4799                .with_sizing_behavior(ListSizingBehavior::Infer)
4800                .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4801                .with_width_from_item(self.max_width_item_index)
4802                .track_scroll(self.scroll_handle.clone())
4803                .when(show_indent_guides, |list| {
4804                    list.with_decoration(
4805                        ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
4806                            .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| {
4807                                let entries = outline_panel.cached_entries.get(range);
4808                                if let Some(entries) = entries {
4809                                    entries.iter().map(|item| item.depth).collect()
4810                                } else {
4811                                    smallvec::SmallVec::new()
4812                                }
4813                            })
4814                            .with_render_fn(cx.entity(), move |outline_panel, params, _, _| {
4815                                const LEFT_OFFSET: Pixels = px(14.);
4816
4817                                let indent_size = params.indent_size;
4818                                let item_height = params.item_height;
4819                                let active_indent_guide_ix = find_active_indent_guide_ix(
4820                                    outline_panel,
4821                                    &params.indent_guides,
4822                                );
4823
4824                                params
4825                                    .indent_guides
4826                                    .into_iter()
4827                                    .enumerate()
4828                                    .map(|(ix, layout)| {
4829                                        let bounds = Bounds::new(
4830                                            point(
4831                                                layout.offset.x * indent_size + LEFT_OFFSET,
4832                                                layout.offset.y * item_height,
4833                                            ),
4834                                            size(px(1.), layout.length * item_height),
4835                                        );
4836                                        ui::RenderedIndentGuide {
4837                                            bounds,
4838                                            layout,
4839                                            is_active: active_indent_guide_ix == Some(ix),
4840                                            hitbox: None,
4841                                        }
4842                                    })
4843                                    .collect()
4844                            }),
4845                    )
4846                })
4847            };
4848
4849            v_flex()
4850                .flex_shrink()
4851                .size_full()
4852                .child(list_contents.size_full().flex_shrink())
4853                .children(self.render_vertical_scrollbar(cx))
4854                .when_some(
4855                    self.render_horizontal_scrollbar(window, cx),
4856                    |this, scrollbar| this.pb_4().child(scrollbar),
4857                )
4858        }
4859        .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4860            deferred(
4861                anchored()
4862                    .position(*position)
4863                    .anchor(gpui::Corner::TopLeft)
4864                    .child(menu.clone()),
4865            )
4866            .with_priority(1)
4867        }));
4868
4869        v_flex().w_full().flex_1().overflow_hidden().child(contents)
4870    }
4871
4872    fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context<Self>) -> Div {
4873        v_flex().flex_none().child(horizontal_separator(cx)).child(
4874            h_flex()
4875                .p_2()
4876                .w_full()
4877                .child(self.filter_editor.clone())
4878                .child(
4879                    div().child(
4880                        IconButton::new(
4881                            "outline-panel-menu",
4882                            if pinned {
4883                                IconName::Unpin
4884                            } else {
4885                                IconName::Pin
4886                            },
4887                        )
4888                        .tooltip(Tooltip::text(if pinned {
4889                            "Unpin Outline"
4890                        } else {
4891                            "Pin Active Outline"
4892                        }))
4893                        .shape(IconButtonShape::Square)
4894                        .on_click(cx.listener(
4895                            |outline_panel, _, window, cx| {
4896                                outline_panel.toggle_active_editor_pin(
4897                                    &ToggleActiveEditorPin,
4898                                    window,
4899                                    cx,
4900                                );
4901                            },
4902                        )),
4903                    ),
4904                ),
4905        )
4906    }
4907
4908    fn buffers_inside_directory(
4909        &self,
4910        dir_worktree: WorktreeId,
4911        dir_entry: &GitEntry,
4912    ) -> HashSet<BufferId> {
4913        if !dir_entry.is_dir() {
4914            debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
4915            return HashSet::default();
4916        }
4917
4918        self.fs_entries
4919            .iter()
4920            .skip_while(|fs_entry| match fs_entry {
4921                FsEntry::Directory(directory) => {
4922                    directory.worktree_id != dir_worktree || &directory.entry != dir_entry
4923                }
4924                _ => true,
4925            })
4926            .skip(1)
4927            .take_while(|fs_entry| match fs_entry {
4928                FsEntry::ExternalFile(..) => false,
4929                FsEntry::Directory(directory) => {
4930                    directory.worktree_id == dir_worktree
4931                        && directory.entry.path.starts_with(&dir_entry.path)
4932                }
4933                FsEntry::File(file) => {
4934                    file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path)
4935                }
4936            })
4937            .filter_map(|fs_entry| match fs_entry {
4938                FsEntry::File(file) => Some(file.buffer_id),
4939                _ => None,
4940            })
4941            .collect()
4942    }
4943}
4944
4945fn workspace_active_editor(
4946    workspace: &Workspace,
4947    cx: &App,
4948) -> Option<(Box<dyn ItemHandle>, Entity<Editor>)> {
4949    let active_item = workspace.active_item(cx)?;
4950    let active_editor = active_item
4951        .act_as::<Editor>(cx)
4952        .filter(|editor| editor.read(cx).mode().is_full())?;
4953    Some((active_item, active_editor))
4954}
4955
4956fn back_to_common_visited_parent(
4957    visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
4958    worktree_id: &WorktreeId,
4959    new_entry: &Entry,
4960) -> Option<(WorktreeId, ProjectEntryId)> {
4961    while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
4962        match new_entry.path.parent() {
4963            Some(parent_path) => {
4964                if parent_path == visited_path.as_ref() {
4965                    return Some((*worktree_id, *visited_dir_id));
4966                }
4967            }
4968            None => {
4969                break;
4970            }
4971        }
4972        visited_dirs.pop();
4973    }
4974    None
4975}
4976
4977fn file_name(path: &Path) -> String {
4978    let mut current_path = path;
4979    loop {
4980        if let Some(file_name) = current_path.file_name() {
4981            return file_name.to_string_lossy().into_owned();
4982        }
4983        match current_path.parent() {
4984            Some(parent) => current_path = parent,
4985            None => return path.to_string_lossy().into_owned(),
4986        }
4987    }
4988}
4989
4990impl Panel for OutlinePanel {
4991    fn persistent_name() -> &'static str {
4992        "Outline Panel"
4993    }
4994
4995    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4996        match OutlinePanelSettings::get_global(cx).dock {
4997            OutlinePanelDockPosition::Left => DockPosition::Left,
4998            OutlinePanelDockPosition::Right => DockPosition::Right,
4999        }
5000    }
5001
5002    fn position_is_valid(&self, position: DockPosition) -> bool {
5003        matches!(position, DockPosition::Left | DockPosition::Right)
5004    }
5005
5006    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
5007        settings::update_settings_file::<OutlinePanelSettings>(
5008            self.fs.clone(),
5009            cx,
5010            move |settings, _| {
5011                let dock = match position {
5012                    DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
5013                    DockPosition::Right => OutlinePanelDockPosition::Right,
5014                };
5015                settings.dock = Some(dock);
5016            },
5017        );
5018    }
5019
5020    fn size(&self, _: &Window, cx: &App) -> Pixels {
5021        self.width
5022            .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
5023    }
5024
5025    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
5026        self.width = size;
5027        cx.notify();
5028        cx.defer_in(window, |this, _, cx| {
5029            this.serialize(cx);
5030        });
5031    }
5032
5033    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
5034        OutlinePanelSettings::get_global(cx)
5035            .button
5036            .then_some(IconName::ListTree)
5037    }
5038
5039    fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> {
5040        Some("Outline Panel")
5041    }
5042
5043    fn toggle_action(&self) -> Box<dyn Action> {
5044        Box::new(ToggleFocus)
5045    }
5046
5047    fn starts_open(&self, _window: &Window, _: &App) -> bool {
5048        self.active
5049    }
5050
5051    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
5052        cx.spawn_in(window, async move |outline_panel, cx| {
5053            outline_panel
5054                .update_in(cx, |outline_panel, window, cx| {
5055                    let old_active = outline_panel.active;
5056                    outline_panel.active = active;
5057                    if old_active != active {
5058                        if active
5059                            && let Some((active_item, active_editor)) =
5060                                outline_panel.workspace.upgrade().and_then(|workspace| {
5061                                    workspace_active_editor(workspace.read(cx), cx)
5062                                })
5063                        {
5064                            if outline_panel.should_replace_active_item(active_item.as_ref()) {
5065                                outline_panel.replace_active_editor(
5066                                    active_item,
5067                                    active_editor,
5068                                    window,
5069                                    cx,
5070                                );
5071                            } else {
5072                                outline_panel.update_fs_entries(active_editor, None, window, cx)
5073                            }
5074                            return;
5075                        }
5076
5077                        if !outline_panel.pinned {
5078                            outline_panel.clear_previous(window, cx);
5079                        }
5080                    }
5081                    outline_panel.serialize(cx);
5082                })
5083                .ok();
5084        })
5085        .detach()
5086    }
5087
5088    fn activation_priority(&self) -> u32 {
5089        5
5090    }
5091}
5092
5093impl Focusable for OutlinePanel {
5094    fn focus_handle(&self, cx: &App) -> FocusHandle {
5095        self.filter_editor.focus_handle(cx)
5096    }
5097}
5098
5099impl EventEmitter<Event> for OutlinePanel {}
5100
5101impl EventEmitter<PanelEvent> for OutlinePanel {}
5102
5103impl Render for OutlinePanel {
5104    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5105        let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| {
5106            (project.is_local(), project.is_via_remote_server())
5107        });
5108        let query = self.query(cx);
5109        let pinned = self.pinned;
5110        let settings = OutlinePanelSettings::get_global(cx);
5111        let indent_size = settings.indent_size;
5112        let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
5113
5114        let search_query = match &self.mode {
5115            ItemsDisplayMode::Search(search_query) => Some(search_query),
5116            _ => None,
5117        };
5118
5119        v_flex()
5120            .id("outline-panel")
5121            .size_full()
5122            .overflow_hidden()
5123            .relative()
5124            .on_hover(cx.listener(|this, hovered, window, cx| {
5125                if *hovered {
5126                    this.show_scrollbar = true;
5127                    this.hide_scrollbar_task.take();
5128                    cx.notify();
5129                } else if !this.focus_handle.contains_focused(window, cx) {
5130                    this.hide_scrollbar(window, cx);
5131                }
5132            }))
5133            .key_context(self.dispatch_context(window, cx))
5134            .on_action(cx.listener(Self::open_selected_entry))
5135            .on_action(cx.listener(Self::cancel))
5136            .on_action(cx.listener(Self::select_next))
5137            .on_action(cx.listener(Self::select_previous))
5138            .on_action(cx.listener(Self::select_first))
5139            .on_action(cx.listener(Self::select_last))
5140            .on_action(cx.listener(Self::select_parent))
5141            .on_action(cx.listener(Self::expand_selected_entry))
5142            .on_action(cx.listener(Self::collapse_selected_entry))
5143            .on_action(cx.listener(Self::expand_all_entries))
5144            .on_action(cx.listener(Self::collapse_all_entries))
5145            .on_action(cx.listener(Self::copy_path))
5146            .on_action(cx.listener(Self::copy_relative_path))
5147            .on_action(cx.listener(Self::toggle_active_editor_pin))
5148            .on_action(cx.listener(Self::unfold_directory))
5149            .on_action(cx.listener(Self::fold_directory))
5150            .on_action(cx.listener(Self::open_excerpts))
5151            .on_action(cx.listener(Self::open_excerpts_split))
5152            .when(is_local, |el| {
5153                el.on_action(cx.listener(Self::reveal_in_finder))
5154            })
5155            .when(is_local || is_via_ssh, |el| {
5156                el.on_action(cx.listener(Self::open_in_terminal))
5157            })
5158            .on_mouse_down(
5159                MouseButton::Right,
5160                cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| {
5161                    if let Some(entry) = outline_panel.selected_entry().cloned() {
5162                        outline_panel.deploy_context_menu(event.position, entry, window, cx)
5163                    } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
5164                        outline_panel.deploy_context_menu(
5165                            event.position,
5166                            PanelEntry::Fs(entry),
5167                            window,
5168                            cx,
5169                        )
5170                    }
5171                }),
5172            )
5173            .track_focus(&self.focus_handle)
5174            .when_some(search_query, |outline_panel, search_state| {
5175                outline_panel.child(
5176                    h_flex()
5177                        .py_1p5()
5178                        .px_2()
5179                        .h(DynamicSpacing::Base32.px(cx))
5180                        .flex_shrink_0()
5181                        .border_b_1()
5182                        .border_color(cx.theme().colors().border)
5183                        .gap_0p5()
5184                        .child(Label::new("Searching:").color(Color::Muted))
5185                        .child(Label::new(search_state.query.to_string())),
5186                )
5187            })
5188            .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
5189            .child(self.render_filter_footer(pinned, cx))
5190    }
5191}
5192
5193fn find_active_indent_guide_ix(
5194    outline_panel: &OutlinePanel,
5195    candidates: &[IndentGuideLayout],
5196) -> Option<usize> {
5197    let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
5198        return None;
5199    };
5200    let target_depth = outline_panel
5201        .cached_entries
5202        .get(*target_ix)
5203        .map(|cached_entry| cached_entry.depth)?;
5204
5205    let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
5206        .cached_entries
5207        .get(target_ix + 1)
5208        .filter(|cached_entry| cached_entry.depth > target_depth)
5209        .map(|entry| entry.depth)
5210    {
5211        (target_ix + 1, target_depth.saturating_sub(1))
5212    } else {
5213        (*target_ix, target_depth.saturating_sub(1))
5214    };
5215
5216    candidates
5217        .iter()
5218        .enumerate()
5219        .find(|(_, guide)| {
5220            guide.offset.y <= target_ix
5221                && target_ix < guide.offset.y + guide.length
5222                && guide.offset.x == target_depth
5223        })
5224        .map(|(ix, _)| ix)
5225}
5226
5227fn subscribe_for_editor_events(
5228    editor: &Entity<Editor>,
5229    window: &mut Window,
5230    cx: &mut Context<OutlinePanel>,
5231) -> Subscription {
5232    let debounce = Some(UPDATE_DEBOUNCE);
5233    cx.subscribe_in(
5234        editor,
5235        window,
5236        move |outline_panel, editor, e: &EditorEvent, window, cx| {
5237            if !outline_panel.active {
5238                return;
5239            }
5240            match e {
5241                EditorEvent::SelectionsChanged { local: true } => {
5242                    outline_panel.reveal_entry_for_selection(editor.clone(), window, cx);
5243                    cx.notify();
5244                }
5245                EditorEvent::ExcerptsAdded { excerpts, .. } => {
5246                    outline_panel
5247                        .new_entries_for_fs_update
5248                        .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
5249                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5250                }
5251                EditorEvent::ExcerptsRemoved { ids, .. } => {
5252                    let mut ids = ids.iter().collect::<HashSet<_>>();
5253                    for excerpts in outline_panel.excerpts.values_mut() {
5254                        excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
5255                        if ids.is_empty() {
5256                            break;
5257                        }
5258                    }
5259                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5260                }
5261                EditorEvent::ExcerptsExpanded { ids } => {
5262                    outline_panel.invalidate_outlines(ids);
5263                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5264                    if update_cached_items {
5265                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5266                    }
5267                }
5268                EditorEvent::ExcerptsEdited { ids } => {
5269                    outline_panel.invalidate_outlines(ids);
5270                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5271                    if update_cached_items {
5272                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5273                    }
5274                }
5275                EditorEvent::BufferFoldToggled { ids, .. } => {
5276                    outline_panel.invalidate_outlines(ids);
5277                    let mut latest_unfolded_buffer_id = None;
5278                    let mut latest_folded_buffer_id = None;
5279                    let mut ignore_selections_change = false;
5280                    outline_panel.new_entries_for_fs_update.extend(
5281                        ids.iter()
5282                            .filter(|id| {
5283                                outline_panel
5284                                    .excerpts
5285                                    .iter()
5286                                    .find_map(|(buffer_id, excerpts)| {
5287                                        if excerpts.contains_key(id) {
5288                                            ignore_selections_change |= outline_panel
5289                                                .preserve_selection_on_buffer_fold_toggles
5290                                                .remove(buffer_id);
5291                                            Some(buffer_id)
5292                                        } else {
5293                                            None
5294                                        }
5295                                    })
5296                                    .map(|buffer_id| {
5297                                        if editor.read(cx).is_buffer_folded(*buffer_id, cx) {
5298                                            latest_folded_buffer_id = Some(*buffer_id);
5299                                            false
5300                                        } else {
5301                                            latest_unfolded_buffer_id = Some(*buffer_id);
5302                                            true
5303                                        }
5304                                    })
5305                                    .unwrap_or(true)
5306                            })
5307                            .copied(),
5308                    );
5309                    if !ignore_selections_change
5310                        && let Some(entry_to_select) = latest_unfolded_buffer_id
5311                            .or(latest_folded_buffer_id)
5312                            .and_then(|toggled_buffer_id| {
5313                                outline_panel.fs_entries.iter().find_map(
5314                                    |fs_entry| match fs_entry {
5315                                        FsEntry::ExternalFile(external) => {
5316                                            if external.buffer_id == toggled_buffer_id {
5317                                                Some(fs_entry.clone())
5318                                            } else {
5319                                                None
5320                                            }
5321                                        }
5322                                        FsEntry::File(FsEntryFile { buffer_id, .. }) => {
5323                                            if *buffer_id == toggled_buffer_id {
5324                                                Some(fs_entry.clone())
5325                                            } else {
5326                                                None
5327                                            }
5328                                        }
5329                                        FsEntry::Directory(..) => None,
5330                                    },
5331                                )
5332                            })
5333                            .map(PanelEntry::Fs)
5334                    {
5335                        outline_panel.select_entry(entry_to_select, true, window, cx);
5336                    }
5337
5338                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5339                }
5340                EditorEvent::Reparsed(buffer_id) => {
5341                    if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
5342                        for excerpt in excerpts.values_mut() {
5343                            excerpt.invalidate_outlines();
5344                        }
5345                    }
5346                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5347                    if update_cached_items {
5348                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5349                    }
5350                }
5351                _ => {}
5352            }
5353        },
5354    )
5355}
5356
5357fn empty_icon() -> AnyElement {
5358    h_flex()
5359        .size(IconSize::default().rems())
5360        .invisible()
5361        .flex_none()
5362        .into_any_element()
5363}
5364
5365fn horizontal_separator(cx: &mut App) -> Div {
5366    div().mx_2().border_primary(cx).border_t_1()
5367}
5368
5369#[derive(Debug, Default)]
5370struct GenerationState {
5371    entries: Vec<CachedEntry>,
5372    match_candidates: Vec<StringMatchCandidate>,
5373    max_width_estimate_and_index: Option<(u64, usize)>,
5374}
5375
5376impl GenerationState {
5377    fn clear(&mut self) {
5378        self.entries.clear();
5379        self.match_candidates.clear();
5380        self.max_width_estimate_and_index = None;
5381    }
5382}
5383
5384#[cfg(test)]
5385mod tests {
5386    use db::indoc;
5387    use gpui::{TestAppContext, VisualTestContext, WindowHandle};
5388    use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
5389    use pretty_assertions::assert_eq;
5390    use project::FakeFs;
5391    use search::project_search::{self, perform_project_search};
5392    use serde_json::json;
5393    use util::path;
5394    use workspace::{OpenOptions, OpenVisible};
5395
5396    use super::*;
5397
5398    const SELECTED_MARKER: &str = "  <==== selected";
5399
5400    #[gpui::test(iterations = 10)]
5401    async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
5402        init_test(cx);
5403
5404        let fs = FakeFs::new(cx.background_executor.clone());
5405        let root = path!("/rust-analyzer");
5406        populate_with_test_ra_project(&fs, root).await;
5407        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5408        project.read_with(cx, |project, _| {
5409            project.languages().add(Arc::new(rust_lang()))
5410        });
5411        let workspace = add_outline_panel(&project, cx).await;
5412        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5413        let outline_panel = outline_panel(&workspace, cx);
5414        outline_panel.update_in(cx, |outline_panel, window, cx| {
5415            outline_panel.set_active(true, window, cx)
5416        });
5417
5418        workspace
5419            .update(cx, |workspace, window, cx| {
5420                ProjectSearchView::deploy_search(
5421                    workspace,
5422                    &workspace::DeploySearch::default(),
5423                    window,
5424                    cx,
5425                )
5426            })
5427            .unwrap();
5428        let search_view = workspace
5429            .update(cx, |workspace, _, cx| {
5430                workspace
5431                    .active_pane()
5432                    .read(cx)
5433                    .items()
5434                    .find_map(|item| item.downcast::<ProjectSearchView>())
5435                    .expect("Project search view expected to appear after new search event trigger")
5436            })
5437            .unwrap();
5438
5439        let query = "param_names_for_lifetime_elision_hints";
5440        perform_project_search(&search_view, query, cx);
5441        search_view.update(cx, |search_view, cx| {
5442            search_view
5443                .results_editor()
5444                .update(cx, |results_editor, cx| {
5445                    assert_eq!(
5446                        results_editor.display_text(cx).match_indices(query).count(),
5447                        9
5448                    );
5449                });
5450        });
5451
5452        let all_matches = format!(
5453            r#"{root}/
5454  crates/
5455    ide/src/
5456      inlay_hints/
5457        fn_lifetime_fn.rs
5458          search: match config.param_names_for_lifetime_elision_hints {{
5459          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
5460          search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
5461          search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
5462      inlay_hints.rs
5463        search: pub param_names_for_lifetime_elision_hints: bool,
5464        search: param_names_for_lifetime_elision_hints: self
5465      static_index.rs
5466        search: param_names_for_lifetime_elision_hints: false,
5467    rust-analyzer/src/
5468      cli/
5469        analysis_stats.rs
5470          search: param_names_for_lifetime_elision_hints: true,
5471      config.rs
5472        search: param_names_for_lifetime_elision_hints: self"#
5473        );
5474
5475        let select_first_in_all_matches = |line_to_select: &str| {
5476            assert!(all_matches.contains(line_to_select));
5477            all_matches.replacen(
5478                line_to_select,
5479                &format!("{line_to_select}{SELECTED_MARKER}"),
5480                1,
5481            )
5482        };
5483
5484        cx.executor()
5485            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5486        cx.run_until_parked();
5487        outline_panel.update(cx, |outline_panel, cx| {
5488            assert_eq!(
5489                display_entries(
5490                    &project,
5491                    &snapshot(outline_panel, cx),
5492                    &outline_panel.cached_entries,
5493                    outline_panel.selected_entry(),
5494                    cx,
5495                ),
5496                select_first_in_all_matches(
5497                    "search: match config.param_names_for_lifetime_elision_hints {"
5498                )
5499            );
5500        });
5501
5502        outline_panel.update_in(cx, |outline_panel, window, cx| {
5503            outline_panel.select_parent(&SelectParent, window, cx);
5504            assert_eq!(
5505                display_entries(
5506                    &project,
5507                    &snapshot(outline_panel, cx),
5508                    &outline_panel.cached_entries,
5509                    outline_panel.selected_entry(),
5510                    cx,
5511                ),
5512                select_first_in_all_matches("fn_lifetime_fn.rs")
5513            );
5514        });
5515        outline_panel.update_in(cx, |outline_panel, window, cx| {
5516            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5517        });
5518        cx.executor()
5519            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5520        cx.run_until_parked();
5521        outline_panel.update(cx, |outline_panel, cx| {
5522            assert_eq!(
5523                display_entries(
5524                    &project,
5525                    &snapshot(outline_panel, cx),
5526                    &outline_panel.cached_entries,
5527                    outline_panel.selected_entry(),
5528                    cx,
5529                ),
5530                format!(
5531                    r#"{root}/
5532  crates/
5533    ide/src/
5534      inlay_hints/
5535        fn_lifetime_fn.rs{SELECTED_MARKER}
5536      inlay_hints.rs
5537        search: pub param_names_for_lifetime_elision_hints: bool,
5538        search: param_names_for_lifetime_elision_hints: self
5539      static_index.rs
5540        search: param_names_for_lifetime_elision_hints: false,
5541    rust-analyzer/src/
5542      cli/
5543        analysis_stats.rs
5544          search: param_names_for_lifetime_elision_hints: true,
5545      config.rs
5546        search: param_names_for_lifetime_elision_hints: self"#,
5547                )
5548            );
5549        });
5550
5551        outline_panel.update_in(cx, |outline_panel, window, cx| {
5552            outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
5553        });
5554        cx.executor()
5555            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5556        cx.run_until_parked();
5557        outline_panel.update_in(cx, |outline_panel, window, cx| {
5558            outline_panel.select_parent(&SelectParent, window, cx);
5559            assert_eq!(
5560                display_entries(
5561                    &project,
5562                    &snapshot(outline_panel, cx),
5563                    &outline_panel.cached_entries,
5564                    outline_panel.selected_entry(),
5565                    cx,
5566                ),
5567                select_first_in_all_matches("inlay_hints/")
5568            );
5569        });
5570
5571        outline_panel.update_in(cx, |outline_panel, window, cx| {
5572            outline_panel.select_parent(&SelectParent, window, cx);
5573            assert_eq!(
5574                display_entries(
5575                    &project,
5576                    &snapshot(outline_panel, cx),
5577                    &outline_panel.cached_entries,
5578                    outline_panel.selected_entry(),
5579                    cx,
5580                ),
5581                select_first_in_all_matches("ide/src/")
5582            );
5583        });
5584
5585        outline_panel.update_in(cx, |outline_panel, window, cx| {
5586            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5587        });
5588        cx.executor()
5589            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5590        cx.run_until_parked();
5591        outline_panel.update(cx, |outline_panel, cx| {
5592            assert_eq!(
5593                display_entries(
5594                    &project,
5595                    &snapshot(outline_panel, cx),
5596                    &outline_panel.cached_entries,
5597                    outline_panel.selected_entry(),
5598                    cx,
5599                ),
5600                format!(
5601                    r#"{root}/
5602  crates/
5603    ide/src/{SELECTED_MARKER}
5604    rust-analyzer/src/
5605      cli/
5606        analysis_stats.rs
5607          search: param_names_for_lifetime_elision_hints: true,
5608      config.rs
5609        search: param_names_for_lifetime_elision_hints: self"#,
5610                )
5611            );
5612        });
5613        outline_panel.update_in(cx, |outline_panel, window, cx| {
5614            outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5615        });
5616        cx.executor()
5617            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5618        cx.run_until_parked();
5619        outline_panel.update(cx, |outline_panel, cx| {
5620            assert_eq!(
5621                display_entries(
5622                    &project,
5623                    &snapshot(outline_panel, cx),
5624                    &outline_panel.cached_entries,
5625                    outline_panel.selected_entry(),
5626                    cx,
5627                ),
5628                select_first_in_all_matches("ide/src/")
5629            );
5630        });
5631    }
5632
5633    #[gpui::test(iterations = 10)]
5634    async fn test_item_filtering(cx: &mut TestAppContext) {
5635        init_test(cx);
5636
5637        let fs = FakeFs::new(cx.background_executor.clone());
5638        let root = path!("/rust-analyzer");
5639        populate_with_test_ra_project(&fs, root).await;
5640        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5641        project.read_with(cx, |project, _| {
5642            project.languages().add(Arc::new(rust_lang()))
5643        });
5644        let workspace = add_outline_panel(&project, cx).await;
5645        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5646        let outline_panel = outline_panel(&workspace, cx);
5647        outline_panel.update_in(cx, |outline_panel, window, cx| {
5648            outline_panel.set_active(true, window, cx)
5649        });
5650
5651        workspace
5652            .update(cx, |workspace, window, cx| {
5653                ProjectSearchView::deploy_search(
5654                    workspace,
5655                    &workspace::DeploySearch::default(),
5656                    window,
5657                    cx,
5658                )
5659            })
5660            .unwrap();
5661        let search_view = workspace
5662            .update(cx, |workspace, _, cx| {
5663                workspace
5664                    .active_pane()
5665                    .read(cx)
5666                    .items()
5667                    .find_map(|item| item.downcast::<ProjectSearchView>())
5668                    .expect("Project search view expected to appear after new search event trigger")
5669            })
5670            .unwrap();
5671
5672        let query = "param_names_for_lifetime_elision_hints";
5673        perform_project_search(&search_view, query, cx);
5674        search_view.update(cx, |search_view, cx| {
5675            search_view
5676                .results_editor()
5677                .update(cx, |results_editor, cx| {
5678                    assert_eq!(
5679                        results_editor.display_text(cx).match_indices(query).count(),
5680                        9
5681                    );
5682                });
5683        });
5684        let all_matches = format!(
5685            r#"{root}/
5686  crates/
5687    ide/src/
5688      inlay_hints/
5689        fn_lifetime_fn.rs
5690          search: match config.param_names_for_lifetime_elision_hints {{
5691          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
5692          search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
5693          search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
5694      inlay_hints.rs
5695        search: pub param_names_for_lifetime_elision_hints: bool,
5696        search: param_names_for_lifetime_elision_hints: self
5697      static_index.rs
5698        search: param_names_for_lifetime_elision_hints: false,
5699    rust-analyzer/src/
5700      cli/
5701        analysis_stats.rs
5702          search: param_names_for_lifetime_elision_hints: true,
5703      config.rs
5704        search: param_names_for_lifetime_elision_hints: self"#
5705        );
5706
5707        cx.executor()
5708            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5709        cx.run_until_parked();
5710        outline_panel.update(cx, |outline_panel, cx| {
5711            assert_eq!(
5712                display_entries(
5713                    &project,
5714                    &snapshot(outline_panel, cx),
5715                    &outline_panel.cached_entries,
5716                    None,
5717                    cx,
5718                ),
5719                all_matches,
5720            );
5721        });
5722
5723        let filter_text = "a";
5724        outline_panel.update_in(cx, |outline_panel, window, cx| {
5725            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5726                filter_editor.set_text(filter_text, window, cx);
5727            });
5728        });
5729        cx.executor()
5730            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5731        cx.run_until_parked();
5732
5733        outline_panel.update(cx, |outline_panel, cx| {
5734            assert_eq!(
5735                display_entries(
5736                    &project,
5737                    &snapshot(outline_panel, cx),
5738                    &outline_panel.cached_entries,
5739                    None,
5740                    cx,
5741                ),
5742                all_matches
5743                    .lines()
5744                    .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
5745                    .filter(|item| item.contains(filter_text))
5746                    .collect::<Vec<_>>()
5747                    .join("\n"),
5748            );
5749        });
5750
5751        outline_panel.update_in(cx, |outline_panel, window, cx| {
5752            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5753                filter_editor.set_text("", window, cx);
5754            });
5755        });
5756        cx.executor()
5757            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5758        cx.run_until_parked();
5759        outline_panel.update(cx, |outline_panel, cx| {
5760            assert_eq!(
5761                display_entries(
5762                    &project,
5763                    &snapshot(outline_panel, cx),
5764                    &outline_panel.cached_entries,
5765                    None,
5766                    cx,
5767                ),
5768                all_matches,
5769            );
5770        });
5771    }
5772
5773    #[gpui::test(iterations = 10)]
5774    async fn test_item_opening(cx: &mut TestAppContext) {
5775        init_test(cx);
5776
5777        let fs = FakeFs::new(cx.background_executor.clone());
5778        let root = path!("/rust-analyzer");
5779        populate_with_test_ra_project(&fs, root).await;
5780        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5781        project.read_with(cx, |project, _| {
5782            project.languages().add(Arc::new(rust_lang()))
5783        });
5784        let workspace = add_outline_panel(&project, cx).await;
5785        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5786        let outline_panel = outline_panel(&workspace, cx);
5787        outline_panel.update_in(cx, |outline_panel, window, cx| {
5788            outline_panel.set_active(true, window, cx)
5789        });
5790
5791        workspace
5792            .update(cx, |workspace, window, cx| {
5793                ProjectSearchView::deploy_search(
5794                    workspace,
5795                    &workspace::DeploySearch::default(),
5796                    window,
5797                    cx,
5798                )
5799            })
5800            .unwrap();
5801        let search_view = workspace
5802            .update(cx, |workspace, _, cx| {
5803                workspace
5804                    .active_pane()
5805                    .read(cx)
5806                    .items()
5807                    .find_map(|item| item.downcast::<ProjectSearchView>())
5808                    .expect("Project search view expected to appear after new search event trigger")
5809            })
5810            .unwrap();
5811
5812        let query = "param_names_for_lifetime_elision_hints";
5813        perform_project_search(&search_view, query, cx);
5814        search_view.update(cx, |search_view, cx| {
5815            search_view
5816                .results_editor()
5817                .update(cx, |results_editor, cx| {
5818                    assert_eq!(
5819                        results_editor.display_text(cx).match_indices(query).count(),
5820                        9
5821                    );
5822                });
5823        });
5824        let all_matches = format!(
5825            r#"{root}/
5826  crates/
5827    ide/src/
5828      inlay_hints/
5829        fn_lifetime_fn.rs
5830          search: match config.param_names_for_lifetime_elision_hints {{
5831          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
5832          search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
5833          search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
5834      inlay_hints.rs
5835        search: pub param_names_for_lifetime_elision_hints: bool,
5836        search: param_names_for_lifetime_elision_hints: self
5837      static_index.rs
5838        search: param_names_for_lifetime_elision_hints: false,
5839    rust-analyzer/src/
5840      cli/
5841        analysis_stats.rs
5842          search: param_names_for_lifetime_elision_hints: true,
5843      config.rs
5844        search: param_names_for_lifetime_elision_hints: self"#
5845        );
5846        let select_first_in_all_matches = |line_to_select: &str| {
5847            assert!(all_matches.contains(line_to_select));
5848            all_matches.replacen(
5849                line_to_select,
5850                &format!("{line_to_select}{SELECTED_MARKER}"),
5851                1,
5852            )
5853        };
5854        cx.executor()
5855            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5856        cx.run_until_parked();
5857
5858        let active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5859            outline_panel
5860                .active_editor()
5861                .expect("should have an active editor open")
5862        });
5863        let initial_outline_selection =
5864            "search: match config.param_names_for_lifetime_elision_hints {";
5865        outline_panel.update_in(cx, |outline_panel, window, cx| {
5866            assert_eq!(
5867                display_entries(
5868                    &project,
5869                    &snapshot(outline_panel, cx),
5870                    &outline_panel.cached_entries,
5871                    outline_panel.selected_entry(),
5872                    cx,
5873                ),
5874                select_first_in_all_matches(initial_outline_selection)
5875            );
5876            assert_eq!(
5877                selected_row_text(&active_editor, cx),
5878                initial_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5879                "Should place the initial editor selection on the corresponding search result"
5880            );
5881
5882            outline_panel.select_next(&SelectNext, window, cx);
5883            outline_panel.select_next(&SelectNext, window, cx);
5884        });
5885
5886        let navigated_outline_selection =
5887            "search: Some(it) if config.param_names_for_lifetime_elision_hints => {";
5888        outline_panel.update(cx, |outline_panel, cx| {
5889            assert_eq!(
5890                display_entries(
5891                    &project,
5892                    &snapshot(outline_panel, cx),
5893                    &outline_panel.cached_entries,
5894                    outline_panel.selected_entry(),
5895                    cx,
5896                ),
5897                select_first_in_all_matches(navigated_outline_selection)
5898            );
5899        });
5900        cx.executor()
5901            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5902        outline_panel.update(cx, |_, cx| {
5903            assert_eq!(
5904                selected_row_text(&active_editor, cx),
5905                navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5906                "Should still have the initial caret position after SelectNext calls"
5907            );
5908        });
5909
5910        outline_panel.update_in(cx, |outline_panel, window, cx| {
5911            outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5912        });
5913        outline_panel.update(cx, |_outline_panel, cx| {
5914            assert_eq!(
5915                selected_row_text(&active_editor, cx),
5916                navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5917                "After opening, should move the caret to the opened outline entry's position"
5918            );
5919        });
5920
5921        outline_panel.update_in(cx, |outline_panel, window, cx| {
5922            outline_panel.select_next(&SelectNext, window, cx);
5923        });
5924        let next_navigated_outline_selection = "search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },";
5925        outline_panel.update(cx, |outline_panel, cx| {
5926            assert_eq!(
5927                display_entries(
5928                    &project,
5929                    &snapshot(outline_panel, cx),
5930                    &outline_panel.cached_entries,
5931                    outline_panel.selected_entry(),
5932                    cx,
5933                ),
5934                select_first_in_all_matches(next_navigated_outline_selection)
5935            );
5936        });
5937        cx.executor()
5938            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5939        outline_panel.update(cx, |_outline_panel, cx| {
5940            assert_eq!(
5941                selected_row_text(&active_editor, cx),
5942                next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5943                "Should again preserve the selection after another SelectNext call"
5944            );
5945        });
5946
5947        outline_panel.update_in(cx, |outline_panel, window, cx| {
5948            outline_panel.open_excerpts(&editor::actions::OpenExcerpts, window, cx);
5949        });
5950        cx.executor()
5951            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5952        cx.run_until_parked();
5953        let new_active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5954            outline_panel
5955                .active_editor()
5956                .expect("should have an active editor open")
5957        });
5958        outline_panel.update(cx, |outline_panel, cx| {
5959            assert_ne!(
5960                active_editor, new_active_editor,
5961                "After opening an excerpt, new editor should be open"
5962            );
5963            assert_eq!(
5964                display_entries(
5965                    &project,
5966                    &snapshot(outline_panel, cx),
5967                    &outline_panel.cached_entries,
5968                    outline_panel.selected_entry(),
5969                    cx,
5970                ),
5971                "fn_lifetime_fn.rs  <==== selected"
5972            );
5973            assert_eq!(
5974                selected_row_text(&new_active_editor, cx),
5975                next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5976                "When opening the excerpt, should navigate to the place corresponding the outline entry"
5977            );
5978        });
5979    }
5980
5981    #[gpui::test]
5982    async fn test_multiple_workrees(cx: &mut TestAppContext) {
5983        init_test(cx);
5984
5985        let fs = FakeFs::new(cx.background_executor.clone());
5986        fs.insert_tree(
5987            path!("/root"),
5988            json!({
5989                "one": {
5990                    "a.txt": "aaa aaa"
5991                },
5992                "two": {
5993                    "b.txt": "a aaa"
5994                }
5995
5996            }),
5997        )
5998        .await;
5999        let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await;
6000        let workspace = add_outline_panel(&project, cx).await;
6001        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6002        let outline_panel = outline_panel(&workspace, cx);
6003        outline_panel.update_in(cx, |outline_panel, window, cx| {
6004            outline_panel.set_active(true, window, cx)
6005        });
6006
6007        let items = workspace
6008            .update(cx, |workspace, window, cx| {
6009                workspace.open_paths(
6010                    vec![PathBuf::from(path!("/root/two"))],
6011                    OpenOptions {
6012                        visible: Some(OpenVisible::OnlyDirectories),
6013                        ..Default::default()
6014                    },
6015                    None,
6016                    window,
6017                    cx,
6018                )
6019            })
6020            .unwrap()
6021            .await;
6022        assert_eq!(items.len(), 1, "Were opening another worktree directory");
6023        assert!(
6024            items[0].is_none(),
6025            "Directory should be opened successfully"
6026        );
6027
6028        workspace
6029            .update(cx, |workspace, window, cx| {
6030                ProjectSearchView::deploy_search(
6031                    workspace,
6032                    &workspace::DeploySearch::default(),
6033                    window,
6034                    cx,
6035                )
6036            })
6037            .unwrap();
6038        let search_view = workspace
6039            .update(cx, |workspace, _, cx| {
6040                workspace
6041                    .active_pane()
6042                    .read(cx)
6043                    .items()
6044                    .find_map(|item| item.downcast::<ProjectSearchView>())
6045                    .expect("Project search view expected to appear after new search event trigger")
6046            })
6047            .unwrap();
6048
6049        let query = "aaa";
6050        perform_project_search(&search_view, query, cx);
6051        search_view.update(cx, |search_view, cx| {
6052            search_view
6053                .results_editor()
6054                .update(cx, |results_editor, cx| {
6055                    assert_eq!(
6056                        results_editor.display_text(cx).match_indices(query).count(),
6057                        3
6058                    );
6059                });
6060        });
6061
6062        cx.executor()
6063            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6064        cx.run_until_parked();
6065        outline_panel.update(cx, |outline_panel, cx| {
6066            assert_eq!(
6067                display_entries(
6068                    &project,
6069                    &snapshot(outline_panel, cx),
6070                    &outline_panel.cached_entries,
6071                    outline_panel.selected_entry(),
6072                    cx,
6073                ),
6074                format!(
6075                    r#"{}/
6076  a.txt
6077    search: aaa aaa  <==== selected
6078    search: aaa aaa
6079{}/
6080  b.txt
6081    search: a aaa"#,
6082                    path!("/root/one"),
6083                    path!("/root/two"),
6084                ),
6085            );
6086        });
6087
6088        outline_panel.update_in(cx, |outline_panel, window, cx| {
6089            outline_panel.select_previous(&SelectPrevious, window, cx);
6090            outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
6091        });
6092        cx.executor()
6093            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6094        cx.run_until_parked();
6095        outline_panel.update(cx, |outline_panel, cx| {
6096            assert_eq!(
6097                display_entries(
6098                    &project,
6099                    &snapshot(outline_panel, cx),
6100                    &outline_panel.cached_entries,
6101                    outline_panel.selected_entry(),
6102                    cx,
6103                ),
6104                format!(
6105                    r#"{}/
6106  a.txt  <==== selected
6107{}/
6108  b.txt
6109    search: a aaa"#,
6110                    path!("/root/one"),
6111                    path!("/root/two"),
6112                ),
6113            );
6114        });
6115
6116        outline_panel.update_in(cx, |outline_panel, window, cx| {
6117            outline_panel.select_next(&SelectNext, window, cx);
6118            outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
6119        });
6120        cx.executor()
6121            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6122        cx.run_until_parked();
6123        outline_panel.update(cx, |outline_panel, cx| {
6124            assert_eq!(
6125                display_entries(
6126                    &project,
6127                    &snapshot(outline_panel, cx),
6128                    &outline_panel.cached_entries,
6129                    outline_panel.selected_entry(),
6130                    cx,
6131                ),
6132                format!(
6133                    r#"{}/
6134  a.txt
6135{}/  <==== selected"#,
6136                    path!("/root/one"),
6137                    path!("/root/two"),
6138                ),
6139            );
6140        });
6141
6142        outline_panel.update_in(cx, |outline_panel, window, cx| {
6143            outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
6144        });
6145        cx.executor()
6146            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6147        cx.run_until_parked();
6148        outline_panel.update(cx, |outline_panel, cx| {
6149            assert_eq!(
6150                display_entries(
6151                    &project,
6152                    &snapshot(outline_panel, cx),
6153                    &outline_panel.cached_entries,
6154                    outline_panel.selected_entry(),
6155                    cx,
6156                ),
6157                format!(
6158                    r#"{}/
6159  a.txt
6160{}/  <==== selected
6161  b.txt
6162    search: a aaa"#,
6163                    path!("/root/one"),
6164                    path!("/root/two"),
6165                )
6166            );
6167        });
6168    }
6169
6170    #[gpui::test]
6171    async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
6172        init_test(cx);
6173
6174        let root = path!("/root");
6175        let fs = FakeFs::new(cx.background_executor.clone());
6176        fs.insert_tree(
6177            root,
6178            json!({
6179                "src": {
6180                    "lib.rs": indoc!("
6181#[derive(Clone, Debug, PartialEq, Eq, Hash)]
6182struct OutlineEntryExcerpt {
6183    id: ExcerptId,
6184    buffer_id: BufferId,
6185    range: ExcerptRange<language::Anchor>,
6186}"),
6187                }
6188            }),
6189        )
6190        .await;
6191        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6192        project.read_with(cx, |project, _| {
6193            project.languages().add(Arc::new(
6194                rust_lang()
6195                    .with_outline_query(
6196                        r#"
6197                (struct_item
6198                    (visibility_modifier)? @context
6199                    "struct" @context
6200                    name: (_) @name) @item
6201
6202                (field_declaration
6203                    (visibility_modifier)? @context
6204                    name: (_) @name) @item
6205"#,
6206                    )
6207                    .unwrap(),
6208            ))
6209        });
6210        let workspace = add_outline_panel(&project, cx).await;
6211        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6212        let outline_panel = outline_panel(&workspace, cx);
6213        cx.update(|window, cx| {
6214            outline_panel.update(cx, |outline_panel, cx| {
6215                outline_panel.set_active(true, window, cx)
6216            });
6217        });
6218
6219        let _editor = workspace
6220            .update(cx, |workspace, window, cx| {
6221                workspace.open_abs_path(
6222                    PathBuf::from(path!("/root/src/lib.rs")),
6223                    OpenOptions {
6224                        visible: Some(OpenVisible::All),
6225                        ..Default::default()
6226                    },
6227                    window,
6228                    cx,
6229                )
6230            })
6231            .unwrap()
6232            .await
6233            .expect("Failed to open Rust source file")
6234            .downcast::<Editor>()
6235            .expect("Should open an editor for Rust source file");
6236
6237        cx.executor()
6238            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6239        cx.run_until_parked();
6240        outline_panel.update(cx, |outline_panel, cx| {
6241            assert_eq!(
6242                display_entries(
6243                    &project,
6244                    &snapshot(outline_panel, cx),
6245                    &outline_panel.cached_entries,
6246                    outline_panel.selected_entry(),
6247                    cx,
6248                ),
6249                indoc!(
6250                    "
6251outline: struct OutlineEntryExcerpt
6252  outline: id
6253  outline: buffer_id
6254  outline: range"
6255                )
6256            );
6257        });
6258
6259        cx.update(|window, cx| {
6260            outline_panel.update(cx, |outline_panel, cx| {
6261                outline_panel.select_next(&SelectNext, window, cx);
6262            });
6263        });
6264        cx.executor()
6265            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6266        cx.run_until_parked();
6267        outline_panel.update(cx, |outline_panel, cx| {
6268            assert_eq!(
6269                display_entries(
6270                    &project,
6271                    &snapshot(outline_panel, cx),
6272                    &outline_panel.cached_entries,
6273                    outline_panel.selected_entry(),
6274                    cx,
6275                ),
6276                indoc!(
6277                    "
6278outline: struct OutlineEntryExcerpt  <==== selected
6279  outline: id
6280  outline: buffer_id
6281  outline: range"
6282                )
6283            );
6284        });
6285
6286        cx.update(|window, cx| {
6287            outline_panel.update(cx, |outline_panel, cx| {
6288                outline_panel.select_next(&SelectNext, window, cx);
6289            });
6290        });
6291        cx.executor()
6292            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6293        cx.run_until_parked();
6294        outline_panel.update(cx, |outline_panel, cx| {
6295            assert_eq!(
6296                display_entries(
6297                    &project,
6298                    &snapshot(outline_panel, cx),
6299                    &outline_panel.cached_entries,
6300                    outline_panel.selected_entry(),
6301                    cx,
6302                ),
6303                indoc!(
6304                    "
6305outline: struct OutlineEntryExcerpt
6306  outline: id  <==== selected
6307  outline: buffer_id
6308  outline: range"
6309                )
6310            );
6311        });
6312
6313        cx.update(|window, cx| {
6314            outline_panel.update(cx, |outline_panel, cx| {
6315                outline_panel.select_next(&SelectNext, window, cx);
6316            });
6317        });
6318        cx.executor()
6319            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6320        cx.run_until_parked();
6321        outline_panel.update(cx, |outline_panel, cx| {
6322            assert_eq!(
6323                display_entries(
6324                    &project,
6325                    &snapshot(outline_panel, cx),
6326                    &outline_panel.cached_entries,
6327                    outline_panel.selected_entry(),
6328                    cx,
6329                ),
6330                indoc!(
6331                    "
6332outline: struct OutlineEntryExcerpt
6333  outline: id
6334  outline: buffer_id  <==== selected
6335  outline: range"
6336                )
6337            );
6338        });
6339
6340        cx.update(|window, cx| {
6341            outline_panel.update(cx, |outline_panel, cx| {
6342                outline_panel.select_next(&SelectNext, window, cx);
6343            });
6344        });
6345        cx.executor()
6346            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6347        cx.run_until_parked();
6348        outline_panel.update(cx, |outline_panel, cx| {
6349            assert_eq!(
6350                display_entries(
6351                    &project,
6352                    &snapshot(outline_panel, cx),
6353                    &outline_panel.cached_entries,
6354                    outline_panel.selected_entry(),
6355                    cx,
6356                ),
6357                indoc!(
6358                    "
6359outline: struct OutlineEntryExcerpt
6360  outline: id
6361  outline: buffer_id
6362  outline: range  <==== selected"
6363                )
6364            );
6365        });
6366
6367        cx.update(|window, cx| {
6368            outline_panel.update(cx, |outline_panel, cx| {
6369                outline_panel.select_next(&SelectNext, window, cx);
6370            });
6371        });
6372        cx.executor()
6373            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6374        cx.run_until_parked();
6375        outline_panel.update(cx, |outline_panel, cx| {
6376            assert_eq!(
6377                display_entries(
6378                    &project,
6379                    &snapshot(outline_panel, cx),
6380                    &outline_panel.cached_entries,
6381                    outline_panel.selected_entry(),
6382                    cx,
6383                ),
6384                indoc!(
6385                    "
6386outline: struct OutlineEntryExcerpt  <==== selected
6387  outline: id
6388  outline: buffer_id
6389  outline: range"
6390                )
6391            );
6392        });
6393
6394        cx.update(|window, cx| {
6395            outline_panel.update(cx, |outline_panel, cx| {
6396                outline_panel.select_previous(&SelectPrevious, window, cx);
6397            });
6398        });
6399        cx.executor()
6400            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6401        cx.run_until_parked();
6402        outline_panel.update(cx, |outline_panel, cx| {
6403            assert_eq!(
6404                display_entries(
6405                    &project,
6406                    &snapshot(outline_panel, cx),
6407                    &outline_panel.cached_entries,
6408                    outline_panel.selected_entry(),
6409                    cx,
6410                ),
6411                indoc!(
6412                    "
6413outline: struct OutlineEntryExcerpt
6414  outline: id
6415  outline: buffer_id
6416  outline: range  <==== selected"
6417                )
6418            );
6419        });
6420
6421        cx.update(|window, cx| {
6422            outline_panel.update(cx, |outline_panel, cx| {
6423                outline_panel.select_previous(&SelectPrevious, window, cx);
6424            });
6425        });
6426        cx.executor()
6427            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6428        cx.run_until_parked();
6429        outline_panel.update(cx, |outline_panel, cx| {
6430            assert_eq!(
6431                display_entries(
6432                    &project,
6433                    &snapshot(outline_panel, cx),
6434                    &outline_panel.cached_entries,
6435                    outline_panel.selected_entry(),
6436                    cx,
6437                ),
6438                indoc!(
6439                    "
6440outline: struct OutlineEntryExcerpt
6441  outline: id
6442  outline: buffer_id  <==== selected
6443  outline: range"
6444                )
6445            );
6446        });
6447
6448        cx.update(|window, cx| {
6449            outline_panel.update(cx, |outline_panel, cx| {
6450                outline_panel.select_previous(&SelectPrevious, window, cx);
6451            });
6452        });
6453        cx.executor()
6454            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6455        cx.run_until_parked();
6456        outline_panel.update(cx, |outline_panel, cx| {
6457            assert_eq!(
6458                display_entries(
6459                    &project,
6460                    &snapshot(outline_panel, cx),
6461                    &outline_panel.cached_entries,
6462                    outline_panel.selected_entry(),
6463                    cx,
6464                ),
6465                indoc!(
6466                    "
6467outline: struct OutlineEntryExcerpt
6468  outline: id  <==== selected
6469  outline: buffer_id
6470  outline: range"
6471                )
6472            );
6473        });
6474
6475        cx.update(|window, cx| {
6476            outline_panel.update(cx, |outline_panel, cx| {
6477                outline_panel.select_previous(&SelectPrevious, window, cx);
6478            });
6479        });
6480        cx.executor()
6481            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6482        cx.run_until_parked();
6483        outline_panel.update(cx, |outline_panel, cx| {
6484            assert_eq!(
6485                display_entries(
6486                    &project,
6487                    &snapshot(outline_panel, cx),
6488                    &outline_panel.cached_entries,
6489                    outline_panel.selected_entry(),
6490                    cx,
6491                ),
6492                indoc!(
6493                    "
6494outline: struct OutlineEntryExcerpt  <==== selected
6495  outline: id
6496  outline: buffer_id
6497  outline: range"
6498                )
6499            );
6500        });
6501
6502        cx.update(|window, cx| {
6503            outline_panel.update(cx, |outline_panel, cx| {
6504                outline_panel.select_previous(&SelectPrevious, window, cx);
6505            });
6506        });
6507        cx.executor()
6508            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6509        cx.run_until_parked();
6510        outline_panel.update(cx, |outline_panel, cx| {
6511            assert_eq!(
6512                display_entries(
6513                    &project,
6514                    &snapshot(outline_panel, cx),
6515                    &outline_panel.cached_entries,
6516                    outline_panel.selected_entry(),
6517                    cx,
6518                ),
6519                indoc!(
6520                    "
6521outline: struct OutlineEntryExcerpt
6522  outline: id
6523  outline: buffer_id
6524  outline: range  <==== selected"
6525                )
6526            );
6527        });
6528    }
6529
6530    #[gpui::test(iterations = 10)]
6531    async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6532        init_test(cx);
6533
6534        let root = path!("/frontend-project");
6535        let fs = FakeFs::new(cx.background_executor.clone());
6536        fs.insert_tree(
6537            root,
6538            json!({
6539                "public": {
6540                    "lottie": {
6541                        "syntax-tree.json": r#"{ "something": "static" }"#
6542                    }
6543                },
6544                "src": {
6545                    "app": {
6546                        "(site)": {
6547                            "(about)": {
6548                                "jobs": {
6549                                    "[slug]": {
6550                                        "page.tsx": r#"static"#
6551                                    }
6552                                }
6553                            },
6554                            "(blog)": {
6555                                "post": {
6556                                    "[slug]": {
6557                                        "page.tsx": r#"static"#
6558                                    }
6559                                }
6560                            },
6561                        }
6562                    },
6563                    "components": {
6564                        "ErrorBoundary.tsx": r#"static"#,
6565                    }
6566                }
6567
6568            }),
6569        )
6570        .await;
6571        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6572        let workspace = add_outline_panel(&project, cx).await;
6573        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6574        let outline_panel = outline_panel(&workspace, cx);
6575        outline_panel.update_in(cx, |outline_panel, window, cx| {
6576            outline_panel.set_active(true, window, cx)
6577        });
6578
6579        workspace
6580            .update(cx, |workspace, window, cx| {
6581                ProjectSearchView::deploy_search(
6582                    workspace,
6583                    &workspace::DeploySearch::default(),
6584                    window,
6585                    cx,
6586                )
6587            })
6588            .unwrap();
6589        let search_view = workspace
6590            .update(cx, |workspace, _, cx| {
6591                workspace
6592                    .active_pane()
6593                    .read(cx)
6594                    .items()
6595                    .find_map(|item| item.downcast::<ProjectSearchView>())
6596                    .expect("Project search view expected to appear after new search event trigger")
6597            })
6598            .unwrap();
6599
6600        let query = "static";
6601        perform_project_search(&search_view, query, cx);
6602        search_view.update(cx, |search_view, cx| {
6603            search_view
6604                .results_editor()
6605                .update(cx, |results_editor, cx| {
6606                    assert_eq!(
6607                        results_editor.display_text(cx).match_indices(query).count(),
6608                        4
6609                    );
6610                });
6611        });
6612
6613        cx.executor()
6614            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6615        cx.run_until_parked();
6616        outline_panel.update(cx, |outline_panel, cx| {
6617            assert_eq!(
6618                display_entries(
6619                    &project,
6620                    &snapshot(outline_panel, cx),
6621                    &outline_panel.cached_entries,
6622                    outline_panel.selected_entry(),
6623                    cx,
6624                ),
6625                format!(
6626                    r#"{root}/
6627  public/lottie/
6628    syntax-tree.json
6629      search: {{ "something": "static" }}  <==== selected
6630  src/
6631    app/(site)/
6632      (about)/jobs/[slug]/
6633        page.tsx
6634          search: static
6635      (blog)/post/[slug]/
6636        page.tsx
6637          search: static
6638    components/
6639      ErrorBoundary.tsx
6640        search: static"#
6641                )
6642            );
6643        });
6644
6645        outline_panel.update_in(cx, |outline_panel, window, cx| {
6646            // Move to 5th element in the list, 3 items down.
6647            for _ in 0..2 {
6648                outline_panel.select_next(&SelectNext, window, cx);
6649            }
6650            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6651        });
6652        cx.executor()
6653            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6654        cx.run_until_parked();
6655        outline_panel.update(cx, |outline_panel, cx| {
6656            assert_eq!(
6657                display_entries(
6658                    &project,
6659                    &snapshot(outline_panel, cx),
6660                    &outline_panel.cached_entries,
6661                    outline_panel.selected_entry(),
6662                    cx,
6663                ),
6664                format!(
6665                    r#"{root}/
6666  public/lottie/
6667    syntax-tree.json
6668      search: {{ "something": "static" }}
6669  src/
6670    app/(site)/  <==== selected
6671    components/
6672      ErrorBoundary.tsx
6673        search: static"#
6674                )
6675            );
6676        });
6677
6678        outline_panel.update_in(cx, |outline_panel, window, cx| {
6679            // Move to the next visible non-FS entry
6680            for _ in 0..3 {
6681                outline_panel.select_next(&SelectNext, window, cx);
6682            }
6683        });
6684        cx.run_until_parked();
6685        outline_panel.update(cx, |outline_panel, cx| {
6686            assert_eq!(
6687                display_entries(
6688                    &project,
6689                    &snapshot(outline_panel, cx),
6690                    &outline_panel.cached_entries,
6691                    outline_panel.selected_entry(),
6692                    cx,
6693                ),
6694                format!(
6695                    r#"{root}/
6696  public/lottie/
6697    syntax-tree.json
6698      search: {{ "something": "static" }}
6699  src/
6700    app/(site)/
6701    components/
6702      ErrorBoundary.tsx
6703        search: static  <==== selected"#
6704                )
6705            );
6706        });
6707
6708        outline_panel.update_in(cx, |outline_panel, window, cx| {
6709            outline_panel
6710                .active_editor()
6711                .expect("Should have an active editor")
6712                .update(cx, |editor, cx| {
6713                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6714                });
6715        });
6716        cx.executor()
6717            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6718        cx.run_until_parked();
6719        outline_panel.update(cx, |outline_panel, cx| {
6720            assert_eq!(
6721                display_entries(
6722                    &project,
6723                    &snapshot(outline_panel, cx),
6724                    &outline_panel.cached_entries,
6725                    outline_panel.selected_entry(),
6726                    cx,
6727                ),
6728                format!(
6729                    r#"{root}/
6730  public/lottie/
6731    syntax-tree.json
6732      search: {{ "something": "static" }}
6733  src/
6734    app/(site)/
6735    components/
6736      ErrorBoundary.tsx  <==== selected"#
6737                )
6738            );
6739        });
6740
6741        outline_panel.update_in(cx, |outline_panel, window, cx| {
6742            outline_panel
6743                .active_editor()
6744                .expect("Should have an active editor")
6745                .update(cx, |editor, cx| {
6746                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6747                });
6748        });
6749        cx.executor()
6750            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6751        cx.run_until_parked();
6752        outline_panel.update(cx, |outline_panel, cx| {
6753            assert_eq!(
6754                display_entries(
6755                    &project,
6756                    &snapshot(outline_panel, cx),
6757                    &outline_panel.cached_entries,
6758                    outline_panel.selected_entry(),
6759                    cx,
6760                ),
6761                format!(
6762                    r#"{root}/
6763  public/lottie/
6764    syntax-tree.json
6765      search: {{ "something": "static" }}
6766  src/
6767    app/(site)/
6768    components/
6769      ErrorBoundary.tsx  <==== selected
6770        search: static"#
6771                )
6772            );
6773        });
6774    }
6775
6776    async fn add_outline_panel(
6777        project: &Entity<Project>,
6778        cx: &mut TestAppContext,
6779    ) -> WindowHandle<Workspace> {
6780        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6781
6782        let outline_panel = window
6783            .update(cx, |_, window, cx| {
6784                cx.spawn_in(window, async |this, cx| {
6785                    OutlinePanel::load(this, cx.clone()).await
6786                })
6787            })
6788            .unwrap()
6789            .await
6790            .expect("Failed to load outline panel");
6791
6792        window
6793            .update(cx, |workspace, window, cx| {
6794                workspace.add_panel(outline_panel, window, cx);
6795            })
6796            .unwrap();
6797        window
6798    }
6799
6800    fn outline_panel(
6801        workspace: &WindowHandle<Workspace>,
6802        cx: &mut TestAppContext,
6803    ) -> Entity<OutlinePanel> {
6804        workspace
6805            .update(cx, |workspace, _, cx| {
6806                workspace
6807                    .panel::<OutlinePanel>(cx)
6808                    .expect("no outline panel")
6809            })
6810            .unwrap()
6811    }
6812
6813    fn display_entries(
6814        project: &Entity<Project>,
6815        multi_buffer_snapshot: &MultiBufferSnapshot,
6816        cached_entries: &[CachedEntry],
6817        selected_entry: Option<&PanelEntry>,
6818        cx: &mut App,
6819    ) -> String {
6820        let mut display_string = String::new();
6821        for entry in cached_entries {
6822            if !display_string.is_empty() {
6823                display_string += "\n";
6824            }
6825            for _ in 0..entry.depth {
6826                display_string += "  ";
6827            }
6828            display_string += &match &entry.entry {
6829                PanelEntry::Fs(entry) => match entry {
6830                    FsEntry::ExternalFile(_) => {
6831                        panic!("Did not cover external files with tests")
6832                    }
6833                    FsEntry::Directory(directory) => {
6834                        match project
6835                            .read(cx)
6836                            .worktree_for_id(directory.worktree_id, cx)
6837                            .and_then(|worktree| {
6838                                if worktree.read(cx).root_entry() == Some(&directory.entry.entry) {
6839                                    Some(worktree.read(cx).abs_path())
6840                                } else {
6841                                    None
6842                                }
6843                            }) {
6844                            Some(root_path) => format!(
6845                                "{}/{}",
6846                                root_path.display(),
6847                                directory.entry.path.display(),
6848                            ),
6849                            None => format!(
6850                                "{}/",
6851                                directory
6852                                    .entry
6853                                    .path
6854                                    .file_name()
6855                                    .unwrap_or_default()
6856                                    .to_string_lossy()
6857                            ),
6858                        }
6859                    }
6860                    FsEntry::File(file) => file
6861                        .entry
6862                        .path
6863                        .file_name()
6864                        .map(|name| name.to_string_lossy().to_string())
6865                        .unwrap_or_default(),
6866                },
6867                PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6868                    .entries
6869                    .iter()
6870                    .filter_map(|dir| dir.path.file_name())
6871                    .map(|name| name.to_string_lossy().to_string() + "/")
6872                    .collect(),
6873                PanelEntry::Outline(outline_entry) => match outline_entry {
6874                    OutlineEntry::Excerpt(_) => continue,
6875                    OutlineEntry::Outline(outline_entry) => {
6876                        format!("outline: {}", outline_entry.outline.text)
6877                    }
6878                },
6879                PanelEntry::Search(search_entry) => {
6880                    format!(
6881                        "search: {}",
6882                        search_entry
6883                            .render_data
6884                            .get_or_init(|| SearchData::new(
6885                                &search_entry.match_range,
6886                                multi_buffer_snapshot
6887                            ))
6888                            .context_text
6889                    )
6890                }
6891            };
6892
6893            if Some(&entry.entry) == selected_entry {
6894                display_string += SELECTED_MARKER;
6895            }
6896        }
6897        display_string
6898    }
6899
6900    fn init_test(cx: &mut TestAppContext) {
6901        cx.update(|cx| {
6902            let settings = SettingsStore::test(cx);
6903            cx.set_global(settings);
6904
6905            theme::init(theme::LoadThemes::JustBase, cx);
6906
6907            language::init(cx);
6908            editor::init(cx);
6909            workspace::init_settings(cx);
6910            Project::init_settings(cx);
6911            project_search::init(cx);
6912            super::init(cx);
6913        });
6914    }
6915
6916    // Based on https://github.com/rust-lang/rust-analyzer/
6917    async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6918        fs.insert_tree(
6919            root,
6920            json!({
6921                    "crates": {
6922                        "ide": {
6923                            "src": {
6924                                "inlay_hints": {
6925                                    "fn_lifetime_fn.rs": r##"
6926        pub(super) fn hints(
6927            acc: &mut Vec<InlayHint>,
6928            config: &InlayHintsConfig,
6929            func: ast::Fn,
6930        ) -> Option<()> {
6931            // ... snip
6932
6933            let mut used_names: FxHashMap<SmolStr, usize> =
6934                match config.param_names_for_lifetime_elision_hints {
6935                    true => generic_param_list
6936                        .iter()
6937                        .flat_map(|gpl| gpl.lifetime_params())
6938                        .filter_map(|param| param.lifetime())
6939                        .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6940                        .collect(),
6941                    false => Default::default(),
6942                };
6943            {
6944                let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6945                if self_param.is_some() && potential_lt_refs.next().is_some() {
6946                    allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6947                        // self can't be used as a lifetime, so no need to check for collisions
6948                        "'self".into()
6949                    } else {
6950                        gen_idx_name()
6951                    });
6952                }
6953                potential_lt_refs.for_each(|(name, ..)| {
6954                    let name = match name {
6955                        Some(it) if config.param_names_for_lifetime_elision_hints => {
6956                            if let Some(c) = used_names.get_mut(it.text().as_str()) {
6957                                *c += 1;
6958                                SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6959                            } else {
6960                                used_names.insert(it.text().as_str().into(), 0);
6961                                SmolStr::from_iter(["\'", it.text().as_str()])
6962                            }
6963                        }
6964                        _ => gen_idx_name(),
6965                    };
6966                    allocated_lifetimes.push(name);
6967                });
6968            }
6969
6970            // ... snip
6971        }
6972
6973        // ... snip
6974
6975            #[test]
6976            fn hints_lifetimes_named() {
6977                check_with_config(
6978                    InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6979                    r#"
6980        fn nested_in<'named>(named: &        &X<      &()>) {}
6981        //          ^'named1, 'named2, 'named3, $
6982                                  //^'named1 ^'named2 ^'named3
6983        "#,
6984                );
6985            }
6986
6987        // ... snip
6988        "##,
6989                                },
6990                        "inlay_hints.rs": r#"
6991    #[derive(Clone, Debug, PartialEq, Eq)]
6992    pub struct InlayHintsConfig {
6993        // ... snip
6994        pub param_names_for_lifetime_elision_hints: bool,
6995        pub max_length: Option<usize>,
6996        // ... snip
6997    }
6998
6999    impl Config {
7000        pub fn inlay_hints(&self) -> InlayHintsConfig {
7001            InlayHintsConfig {
7002                // ... snip
7003                param_names_for_lifetime_elision_hints: self
7004                    .inlayHints_lifetimeElisionHints_useParameterNames()
7005                    .to_owned(),
7006                max_length: self.inlayHints_maxLength().to_owned(),
7007                // ... snip
7008            }
7009        }
7010    }
7011    "#,
7012                        "static_index.rs": r#"
7013// ... snip
7014        fn add_file(&mut self, file_id: FileId) {
7015            let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
7016            let folds = self.analysis.folding_ranges(file_id).unwrap();
7017            let inlay_hints = self
7018                .analysis
7019                .inlay_hints(
7020                    &InlayHintsConfig {
7021                        // ... snip
7022                        closure_style: hir::ClosureStyle::ImplFn,
7023                        param_names_for_lifetime_elision_hints: false,
7024                        binding_mode_hints: false,
7025                        max_length: Some(25),
7026                        closure_capture_hints: false,
7027                        // ... snip
7028                    },
7029                    file_id,
7030                    None,
7031                )
7032                .unwrap();
7033            // ... snip
7034    }
7035// ... snip
7036    "#
7037                            }
7038                        },
7039                        "rust-analyzer": {
7040                            "src": {
7041                                "cli": {
7042                                    "analysis_stats.rs": r#"
7043        // ... snip
7044                for &file_id in &file_ids {
7045                    _ = analysis.inlay_hints(
7046                        &InlayHintsConfig {
7047                            // ... snip
7048                            implicit_drop_hints: true,
7049                            lifetime_elision_hints: ide::LifetimeElisionHints::Always,
7050                            param_names_for_lifetime_elision_hints: true,
7051                            hide_named_constructor_hints: false,
7052                            hide_closure_initialization_hints: false,
7053                            closure_style: hir::ClosureStyle::ImplFn,
7054                            max_length: Some(25),
7055                            closing_brace_hints_min_lines: Some(20),
7056                            fields_to_resolve: InlayFieldsToResolve::empty(),
7057                            range_exclusive_hints: true,
7058                        },
7059                        file_id.into(),
7060                        None,
7061                    );
7062                }
7063        // ... snip
7064                                    "#,
7065                                },
7066                                "config.rs": r#"
7067                config_data! {
7068                    /// Configs that only make sense when they are set by a client. As such they can only be defined
7069                    /// by setting them using client's settings (e.g `settings.json` on VS Code).
7070                    client: struct ClientDefaultConfigData <- ClientConfigInput -> {
7071                        // ... snip
7072                        /// Maximum length for inlay hints. Set to null to have an unlimited length.
7073                        inlayHints_maxLength: Option<usize>                        = Some(25),
7074                        // ... snip
7075                        /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
7076                        inlayHints_lifetimeElisionHints_useParameterNames: bool    = false,
7077                        // ... snip
7078                    }
7079                }
7080
7081                impl Config {
7082                    // ... snip
7083                    pub fn inlay_hints(&self) -> InlayHintsConfig {
7084                        InlayHintsConfig {
7085                            // ... snip
7086                            param_names_for_lifetime_elision_hints: self
7087                                .inlayHints_lifetimeElisionHints_useParameterNames()
7088                                .to_owned(),
7089                            max_length: self.inlayHints_maxLength().to_owned(),
7090                            // ... snip
7091                        }
7092                    }
7093                    // ... snip
7094                }
7095                "#
7096                                }
7097                        }
7098                    }
7099            }),
7100        )
7101        .await;
7102    }
7103
7104    fn rust_lang() -> Language {
7105        Language::new(
7106            LanguageConfig {
7107                name: "Rust".into(),
7108                matcher: LanguageMatcher {
7109                    path_suffixes: vec!["rs".to_string()],
7110                    ..Default::default()
7111                },
7112                ..Default::default()
7113            },
7114            Some(tree_sitter_rust::LANGUAGE.into()),
7115        )
7116        .with_highlights_query(
7117            r#"
7118                (field_identifier) @field
7119                (struct_expression) @struct
7120            "#,
7121        )
7122        .unwrap()
7123        .with_injection_query(
7124            r#"
7125                (macro_invocation
7126                    (token_tree) @injection.content
7127                    (#set! injection.language "rust"))
7128            "#,
7129        )
7130        .unwrap()
7131    }
7132
7133    fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
7134        outline_panel
7135            .active_editor()
7136            .unwrap()
7137            .read(cx)
7138            .buffer()
7139            .read(cx)
7140            .snapshot(cx)
7141    }
7142
7143    fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
7144        editor.update(cx, |editor, cx| {
7145                let selections = editor.selections.all::<language::Point>(cx);
7146                assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
7147                let selection = selections.first().unwrap();
7148                let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
7149                let line_start = language::Point::new(selection.start.row, 0);
7150                let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
7151                multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
7152        })
7153    }
7154
7155    #[gpui::test]
7156    async fn test_outline_keyboard_expand_collapse(cx: &mut TestAppContext) {
7157        init_test(cx);
7158
7159        let fs = FakeFs::new(cx.background_executor.clone());
7160        fs.insert_tree(
7161            "/test",
7162            json!({
7163                "src": {
7164                    "lib.rs": indoc!("
7165                            mod outer {
7166                                pub struct OuterStruct {
7167                                    field: String,
7168                                }
7169                                impl OuterStruct {
7170                                    pub fn new() -> Self {
7171                                        Self { field: String::new() }
7172                                    }
7173                                    pub fn method(&self) {
7174                                        println!(\"{}\", self.field);
7175                                    }
7176                                }
7177                                mod inner {
7178                                    pub fn inner_function() {
7179                                        let x = 42;
7180                                        println!(\"{}\", x);
7181                                    }
7182                                    pub struct InnerStruct {
7183                                        value: i32,
7184                                    }
7185                                }
7186                            }
7187                            fn main() {
7188                                let s = outer::OuterStruct::new();
7189                                s.method();
7190                            }
7191                        "),
7192                }
7193            }),
7194        )
7195        .await;
7196
7197        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7198        project.read_with(cx, |project, _| {
7199            project.languages().add(Arc::new(
7200                rust_lang()
7201                    .with_outline_query(
7202                        r#"
7203                            (struct_item
7204                                (visibility_modifier)? @context
7205                                "struct" @context
7206                                name: (_) @name) @item
7207                            (impl_item
7208                                "impl" @context
7209                                trait: (_)? @context
7210                                "for"? @context
7211                                type: (_) @context
7212                                body: (_)) @item
7213                            (function_item
7214                                (visibility_modifier)? @context
7215                                "fn" @context
7216                                name: (_) @name
7217                                parameters: (_) @context) @item
7218                            (mod_item
7219                                (visibility_modifier)? @context
7220                                "mod" @context
7221                                name: (_) @name) @item
7222                            (enum_item
7223                                (visibility_modifier)? @context
7224                                "enum" @context
7225                                name: (_) @name) @item
7226                            (field_declaration
7227                                (visibility_modifier)? @context
7228                                name: (_) @name
7229                                ":" @context
7230                                type: (_) @context) @item
7231                            "#,
7232                    )
7233                    .unwrap(),
7234            ))
7235        });
7236        let workspace = add_outline_panel(&project, cx).await;
7237        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7238        let outline_panel = outline_panel(&workspace, cx);
7239
7240        outline_panel.update_in(cx, |outline_panel, window, cx| {
7241            outline_panel.set_active(true, window, cx)
7242        });
7243
7244        workspace
7245            .update(cx, |workspace, window, cx| {
7246                workspace.open_abs_path(
7247                    PathBuf::from("/test/src/lib.rs"),
7248                    OpenOptions {
7249                        visible: Some(OpenVisible::All),
7250                        ..Default::default()
7251                    },
7252                    window,
7253                    cx,
7254                )
7255            })
7256            .unwrap()
7257            .await
7258            .unwrap();
7259
7260        cx.executor()
7261            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7262        cx.run_until_parked();
7263
7264        // Force another update cycle to ensure outlines are fetched
7265        outline_panel.update_in(cx, |panel, window, cx| {
7266            panel.update_non_fs_items(window, cx);
7267            panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7268        });
7269        cx.executor()
7270            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7271        cx.run_until_parked();
7272
7273        outline_panel.update(cx, |outline_panel, cx| {
7274            assert_eq!(
7275                display_entries(
7276                    &project,
7277                    &snapshot(outline_panel, cx),
7278                    &outline_panel.cached_entries,
7279                    outline_panel.selected_entry(),
7280                    cx,
7281                ),
7282                indoc!(
7283                    "
7284outline: mod outer  <==== selected
7285  outline: pub struct OuterStruct
7286    outline: field: String
7287  outline: impl OuterStruct
7288    outline: pub fn new()
7289    outline: pub fn method(&self)
7290  outline: mod inner
7291    outline: pub fn inner_function()
7292    outline: pub struct InnerStruct
7293      outline: value: i32
7294outline: fn main()"
7295                )
7296            );
7297        });
7298
7299        let parent_outline = outline_panel
7300            .read_with(cx, |panel, _cx| {
7301                panel
7302                    .cached_entries
7303                    .iter()
7304                    .find_map(|entry| match &entry.entry {
7305                        PanelEntry::Outline(OutlineEntry::Outline(outline))
7306                            if panel
7307                                .outline_children_cache
7308                                .get(&outline.buffer_id)
7309                                .and_then(|children_map| {
7310                                    let key =
7311                                        (outline.outline.range.clone(), outline.outline.depth);
7312                                    children_map.get(&key)
7313                                })
7314                                .copied()
7315                                .unwrap_or(false) =>
7316                        {
7317                            Some(entry.entry.clone())
7318                        }
7319                        _ => None,
7320                    })
7321            })
7322            .expect("Should find an outline with children");
7323
7324        outline_panel.update_in(cx, |panel, window, cx| {
7325            panel.select_entry(parent_outline.clone(), true, window, cx);
7326            panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7327        });
7328        cx.executor()
7329            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7330        cx.run_until_parked();
7331
7332        outline_panel.update(cx, |outline_panel, cx| {
7333            assert_eq!(
7334                display_entries(
7335                    &project,
7336                    &snapshot(outline_panel, cx),
7337                    &outline_panel.cached_entries,
7338                    outline_panel.selected_entry(),
7339                    cx,
7340                ),
7341                indoc!(
7342                    "
7343outline: mod outer  <==== selected
7344outline: fn main()"
7345                )
7346            );
7347        });
7348
7349        outline_panel.update_in(cx, |panel, window, cx| {
7350            panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7351        });
7352        cx.executor()
7353            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7354        cx.run_until_parked();
7355
7356        outline_panel.update(cx, |outline_panel, cx| {
7357            assert_eq!(
7358                display_entries(
7359                    &project,
7360                    &snapshot(outline_panel, cx),
7361                    &outline_panel.cached_entries,
7362                    outline_panel.selected_entry(),
7363                    cx,
7364                ),
7365                indoc!(
7366                    "
7367outline: mod outer  <==== selected
7368  outline: pub struct OuterStruct
7369    outline: field: String
7370  outline: impl OuterStruct
7371    outline: pub fn new()
7372    outline: pub fn method(&self)
7373  outline: mod inner
7374    outline: pub fn inner_function()
7375    outline: pub struct InnerStruct
7376      outline: value: i32
7377outline: fn main()"
7378                )
7379            );
7380        });
7381
7382        outline_panel.update_in(cx, |panel, window, cx| {
7383            panel.collapsed_entries.clear();
7384            panel.update_cached_entries(None, window, cx);
7385        });
7386        cx.executor()
7387            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7388        cx.run_until_parked();
7389
7390        outline_panel.update_in(cx, |panel, window, cx| {
7391            let outlines_with_children: Vec<_> = panel
7392                .cached_entries
7393                .iter()
7394                .filter_map(|entry| match &entry.entry {
7395                    PanelEntry::Outline(OutlineEntry::Outline(outline))
7396                        if panel
7397                            .outline_children_cache
7398                            .get(&outline.buffer_id)
7399                            .and_then(|children_map| {
7400                                let key = (outline.outline.range.clone(), outline.outline.depth);
7401                                children_map.get(&key)
7402                            })
7403                            .copied()
7404                            .unwrap_or(false) =>
7405                    {
7406                        Some(entry.entry.clone())
7407                    }
7408                    _ => None,
7409                })
7410                .collect();
7411
7412            for outline in outlines_with_children {
7413                panel.select_entry(outline, false, window, cx);
7414                panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7415            }
7416        });
7417        cx.executor()
7418            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7419        cx.run_until_parked();
7420
7421        outline_panel.update(cx, |outline_panel, cx| {
7422            assert_eq!(
7423                display_entries(
7424                    &project,
7425                    &snapshot(outline_panel, cx),
7426                    &outline_panel.cached_entries,
7427                    outline_panel.selected_entry(),
7428                    cx,
7429                ),
7430                indoc!(
7431                    "
7432outline: mod outer
7433outline: fn main()"
7434                )
7435            );
7436        });
7437
7438        let collapsed_entries_count =
7439            outline_panel.read_with(cx, |panel, _| panel.collapsed_entries.len());
7440        assert!(
7441            collapsed_entries_count > 0,
7442            "Should have collapsed entries tracked"
7443        );
7444    }
7445
7446    #[gpui::test]
7447    async fn test_outline_click_toggle_behavior(cx: &mut TestAppContext) {
7448        init_test(cx);
7449
7450        let fs = FakeFs::new(cx.background_executor.clone());
7451        fs.insert_tree(
7452            "/test",
7453            json!({
7454                "src": {
7455                    "main.rs": indoc!("
7456                            struct Config {
7457                                name: String,
7458                                value: i32,
7459                            }
7460                            impl Config {
7461                                fn new(name: String) -> Self {
7462                                    Self { name, value: 0 }
7463                                }
7464                                fn get_value(&self) -> i32 {
7465                                    self.value
7466                                }
7467                            }
7468                            enum Status {
7469                                Active,
7470                                Inactive,
7471                            }
7472                            fn process_config(config: Config) -> Status {
7473                                if config.get_value() > 0 {
7474                                    Status::Active
7475                                } else {
7476                                    Status::Inactive
7477                                }
7478                            }
7479                            fn main() {
7480                                let config = Config::new(\"test\".to_string());
7481                                let status = process_config(config);
7482                            }
7483                        "),
7484                }
7485            }),
7486        )
7487        .await;
7488
7489        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7490        project.read_with(cx, |project, _| {
7491            project.languages().add(Arc::new(
7492                rust_lang()
7493                    .with_outline_query(
7494                        r#"
7495                            (struct_item
7496                                (visibility_modifier)? @context
7497                                "struct" @context
7498                                name: (_) @name) @item
7499                            (impl_item
7500                                "impl" @context
7501                                trait: (_)? @context
7502                                "for"? @context
7503                                type: (_) @context
7504                                body: (_)) @item
7505                            (function_item
7506                                (visibility_modifier)? @context
7507                                "fn" @context
7508                                name: (_) @name
7509                                parameters: (_) @context) @item
7510                            (mod_item
7511                                (visibility_modifier)? @context
7512                                "mod" @context
7513                                name: (_) @name) @item
7514                            (enum_item
7515                                (visibility_modifier)? @context
7516                                "enum" @context
7517                                name: (_) @name) @item
7518                            (field_declaration
7519                                (visibility_modifier)? @context
7520                                name: (_) @name
7521                                ":" @context
7522                                type: (_) @context) @item
7523                            "#,
7524                    )
7525                    .unwrap(),
7526            ))
7527        });
7528
7529        let workspace = add_outline_panel(&project, cx).await;
7530        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7531        let outline_panel = outline_panel(&workspace, cx);
7532
7533        outline_panel.update_in(cx, |outline_panel, window, cx| {
7534            outline_panel.set_active(true, window, cx)
7535        });
7536
7537        let _editor = workspace
7538            .update(cx, |workspace, window, cx| {
7539                workspace.open_abs_path(
7540                    PathBuf::from("/test/src/main.rs"),
7541                    OpenOptions {
7542                        visible: Some(OpenVisible::All),
7543                        ..Default::default()
7544                    },
7545                    window,
7546                    cx,
7547                )
7548            })
7549            .unwrap()
7550            .await
7551            .unwrap();
7552
7553        cx.executor()
7554            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7555        cx.run_until_parked();
7556
7557        outline_panel.update(cx, |outline_panel, _cx| {
7558            outline_panel.selected_entry = SelectedEntry::None;
7559        });
7560
7561        // Check initial state - all entries should be expanded by default
7562        outline_panel.update(cx, |outline_panel, cx| {
7563            assert_eq!(
7564                display_entries(
7565                    &project,
7566                    &snapshot(outline_panel, cx),
7567                    &outline_panel.cached_entries,
7568                    outline_panel.selected_entry(),
7569                    cx,
7570                ),
7571                indoc!(
7572                    "
7573outline: struct Config
7574  outline: name: String
7575  outline: value: i32
7576outline: impl Config
7577  outline: fn new(name: String)
7578  outline: fn get_value(&self)
7579outline: enum Status
7580outline: fn process_config(config: Config)
7581outline: fn main()"
7582                )
7583            );
7584        });
7585
7586        outline_panel.update(cx, |outline_panel, _cx| {
7587            outline_panel.selected_entry = SelectedEntry::None;
7588        });
7589
7590        cx.update(|window, cx| {
7591            outline_panel.update(cx, |outline_panel, cx| {
7592                outline_panel.select_first(&SelectFirst, window, cx);
7593            });
7594        });
7595
7596        cx.executor()
7597            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7598        cx.run_until_parked();
7599
7600        outline_panel.update(cx, |outline_panel, cx| {
7601            assert_eq!(
7602                display_entries(
7603                    &project,
7604                    &snapshot(outline_panel, cx),
7605                    &outline_panel.cached_entries,
7606                    outline_panel.selected_entry(),
7607                    cx,
7608                ),
7609                indoc!(
7610                    "
7611outline: struct Config  <==== selected
7612  outline: name: String
7613  outline: value: i32
7614outline: impl Config
7615  outline: fn new(name: String)
7616  outline: fn get_value(&self)
7617outline: enum Status
7618outline: fn process_config(config: Config)
7619outline: fn main()"
7620                )
7621            );
7622        });
7623
7624        cx.update(|window, cx| {
7625            outline_panel.update(cx, |outline_panel, cx| {
7626                outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
7627            });
7628        });
7629
7630        cx.executor()
7631            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7632        cx.run_until_parked();
7633
7634        outline_panel.update(cx, |outline_panel, cx| {
7635            assert_eq!(
7636                display_entries(
7637                    &project,
7638                    &snapshot(outline_panel, cx),
7639                    &outline_panel.cached_entries,
7640                    outline_panel.selected_entry(),
7641                    cx,
7642                ),
7643                indoc!(
7644                    "
7645outline: struct Config  <==== selected
7646outline: impl Config
7647  outline: fn new(name: String)
7648  outline: fn get_value(&self)
7649outline: enum Status
7650outline: fn process_config(config: Config)
7651outline: fn main()"
7652                )
7653            );
7654        });
7655
7656        cx.update(|window, cx| {
7657            outline_panel.update(cx, |outline_panel, cx| {
7658                outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
7659            });
7660        });
7661
7662        cx.executor()
7663            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7664        cx.run_until_parked();
7665
7666        outline_panel.update(cx, |outline_panel, cx| {
7667            assert_eq!(
7668                display_entries(
7669                    &project,
7670                    &snapshot(outline_panel, cx),
7671                    &outline_panel.cached_entries,
7672                    outline_panel.selected_entry(),
7673                    cx,
7674                ),
7675                indoc!(
7676                    "
7677outline: struct Config  <==== selected
7678  outline: name: String
7679  outline: value: i32
7680outline: impl Config
7681  outline: fn new(name: String)
7682  outline: fn get_value(&self)
7683outline: enum Status
7684outline: fn process_config(config: Config)
7685outline: fn main()"
7686                )
7687            );
7688        });
7689    }
7690}