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